Files
storycove/frontend/src/app/authors/[id]/page.tsx
Stefan Hardegger d69bed00a2 MVP Version
2025-07-23 12:28:48 +02:00

240 lines
8.1 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import { authorApi, storyApi, getImageUrl } from '../../../lib/api';
import { Author, Story } from '../../../types/api';
import AppLayout from '../../../components/layout/AppLayout';
import Button from '../../../components/ui/Button';
import StoryCard from '../../../components/stories/StoryCard';
import LoadingSpinner from '../../../components/ui/LoadingSpinner';
export default function AuthorDetailPage() {
const params = useParams();
const router = useRouter();
const authorId = params.id as string;
const [author, setAuthor] = useState<Author | null>(null);
const [stories, setStories] = useState<Story[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadAuthorData = async () => {
try {
setLoading(true);
const [authorData, storiesResult] = await Promise.all([
authorApi.getAuthor(authorId),
storyApi.getStories({ page: 0, size: 1000 }) // Get all stories to filter by author
]);
setAuthor(authorData);
// Filter stories by this author
const authorStories = storiesResult.content.filter(
story => story.authorId === authorId
);
setStories(authorStories);
} catch (error) {
console.error('Failed to load author data:', error);
router.push('/authors');
} finally {
setLoading(false);
}
};
if (authorId) {
loadAuthorData();
}
}, [authorId, router]);
if (loading) {
return (
<AppLayout>
<div className="flex items-center justify-center py-20">
<LoadingSpinner size="lg" />
</div>
</AppLayout>
);
}
if (!author) {
return (
<AppLayout>
<div className="text-center py-20">
<h1 className="text-2xl font-bold theme-header mb-4">Author Not Found</h1>
<Button href="/authors">Back to Authors</Button>
</div>
</AppLayout>
);
}
return (
<AppLayout>
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex items-center gap-4">
{/* Avatar */}
<div className="w-20 h-20 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 flex-shrink-0">
{author.avatarImagePath ? (
<Image
src={getImageUrl(author.avatarImagePath)}
alt={author.name}
width={80}
height={80}
className="w-full h-full object-cover"
unoptimized
/>
) : (
<div className="w-full h-full flex items-center justify-center text-3xl theme-text">
👤
</div>
)}
</div>
<div>
<h1 className="text-3xl font-bold theme-header">{author.name}</h1>
<p className="theme-text mt-1">
{stories.length} {stories.length === 1 ? 'story' : 'stories'}
</p>
{/* Author Rating */}
{author.authorRating && (
<div className="flex items-center gap-1 mt-2">
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
className={`text-lg ${
star <= (author.authorRating || 0)
? 'text-yellow-400'
: 'text-gray-300 dark:text-gray-600'
}`}
>
</span>
))}
</div>
<span className="text-sm theme-text ml-1">
({author.authorRating}/5)
</span>
</div>
)}
{/* Average Story Rating */}
{author.averageStoryRating && (
<div className="flex items-center gap-1 mt-1">
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
className={`text-sm ${
star <= Math.round(author.averageStoryRating || 0)
? 'text-blue-400'
: 'text-gray-300 dark:text-gray-600'
}`}
>
</span>
))}
</div>
<span className="text-xs theme-text ml-1">
Avg Story Rating: {author.averageStoryRating.toFixed(1)}/5
</span>
</div>
)}
</div>
</div>
<div className="flex gap-2">
<Button href="/authors" variant="ghost">
Back to Authors
</Button>
<Button href={`/authors/${authorId}/edit`}>
Edit Author
</Button>
</div>
</div>
{/* Author Details */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-1 space-y-6">
{/* Notes Section */}
<div className="theme-card theme-shadow rounded-lg p-6">
<h2 className="text-xl font-semibold theme-header mb-4">Notes</h2>
<div className="theme-text">
{author.notes ? (
<p className="whitespace-pre-wrap">{author.notes}</p>
) : (
<p className="text-gray-500 dark:text-gray-400 italic">
No notes added yet.
</p>
)}
</div>
</div>
{/* URLs Section */}
<div className="theme-card theme-shadow rounded-lg p-6">
<h2 className="text-xl font-semibold theme-header mb-4">URLs</h2>
<div className="space-y-2">
{author.urls && author.urls.length > 0 ? (
author.urls.map((url, index) => (
<div key={index}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="theme-accent hover:underline break-all"
>
{url}
</a>
</div>
))
) : (
<p className="text-gray-500 dark:text-gray-400 italic">
No URLs added yet.
</p>
)}
</div>
</div>
</div>
{/* Stories Section */}
<div className="lg:col-span-2 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold theme-header">Stories</h2>
<p className="theme-text">
{stories.length} {stories.length === 1 ? 'story' : 'stories'}
</p>
</div>
{stories.length === 0 ? (
<div className="text-center py-12 theme-card theme-shadow rounded-lg">
<p className="theme-text text-lg mb-4">No stories by this author yet.</p>
<Button href="/add-story">Add a Story</Button>
</div>
) : (
<div className="space-y-4">
{stories.map((story) => (
<StoryCard
key={story.id}
story={story}
viewMode="list"
onUpdate={() => {
// Reload stories after update
window.location.reload();
}}
/>
))}
</div>
)}
</div>
</div>
</div>
</AppLayout>
);
}