240 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
} |