203 lines
7.2 KiB
TypeScript
203 lines
7.2 KiB
TypeScript
'use client';
|
|
|
|
import { Collection } from '../../types/api';
|
|
import { getImageUrl } from '../../lib/api';
|
|
import Link from 'next/link';
|
|
|
|
interface CollectionCardProps {
|
|
collection: Collection;
|
|
viewMode: 'grid' | 'list';
|
|
onUpdate?: () => void;
|
|
}
|
|
|
|
export default function CollectionCard({ collection, viewMode, onUpdate }: CollectionCardProps) {
|
|
const formatReadingTime = (minutes: number): string => {
|
|
if (minutes < 60) {
|
|
return `${minutes}m`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = minutes % 60;
|
|
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
};
|
|
|
|
const renderRatingStars = (rating?: number) => {
|
|
if (!rating) return null;
|
|
|
|
return (
|
|
<div className="flex items-center">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<span
|
|
key={star}
|
|
className={`text-sm ${
|
|
star <= rating ? 'text-yellow-400' : 'text-gray-300'
|
|
}`}
|
|
>
|
|
★
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (viewMode === 'grid') {
|
|
return (
|
|
<Link href={`/collections/${collection.id}`}>
|
|
<div className="theme-card p-4 hover:border-gray-400 transition-colors cursor-pointer">
|
|
{/* Cover Image or Placeholder */}
|
|
<div className="aspect-[3/4] mb-3 relative overflow-hidden rounded-lg bg-gray-100">
|
|
{collection.coverImagePath ? (
|
|
<img
|
|
src={getImageUrl(collection.coverImagePath)}
|
|
alt={`${collection.name} cover`}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-100 to-purple-100">
|
|
<div className="text-center p-4">
|
|
<div className="text-2xl font-bold theme-text mb-1">
|
|
{collection.storyCount}
|
|
</div>
|
|
<div className="text-xs theme-text opacity-60">
|
|
{collection.storyCount === 1 ? 'story' : 'stories'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{collection.isArchived && (
|
|
<div className="absolute top-2 right-2 bg-yellow-500 text-white px-2 py-1 rounded text-xs">
|
|
Archived
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Collection Info */}
|
|
<div className="space-y-2">
|
|
<h3 className="font-semibold theme-header line-clamp-2">
|
|
{collection.name}
|
|
</h3>
|
|
|
|
{collection.description && (
|
|
<p className="text-sm theme-text opacity-70 line-clamp-2">
|
|
{collection.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between text-xs theme-text opacity-60">
|
|
<span>{collection.storyCount} stories</span>
|
|
<span>{collection.estimatedReadingTime ? formatReadingTime(collection.estimatedReadingTime) : '—'}</span>
|
|
</div>
|
|
|
|
{collection.rating && (
|
|
<div className="flex justify-center">
|
|
{renderRatingStars(collection.rating)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Tags */}
|
|
{collection.tags && collection.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1">
|
|
{collection.tags.slice(0, 3).map((tag) => (
|
|
<span
|
|
key={tag.id}
|
|
className="inline-block px-2 py-1 text-xs rounded-full theme-accent-bg text-white"
|
|
>
|
|
{tag.name}
|
|
</span>
|
|
))}
|
|
{collection.tags.length > 3 && (
|
|
<span className="text-xs theme-text opacity-60">
|
|
+{collection.tags.length - 3} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
// List view
|
|
return (
|
|
<Link href={`/collections/${collection.id}`}>
|
|
<div className="theme-card p-4 hover:border-gray-400 transition-colors cursor-pointer">
|
|
<div className="flex gap-4">
|
|
{/* Cover Image */}
|
|
<div className="w-16 h-20 flex-shrink-0 rounded overflow-hidden bg-gray-100">
|
|
{collection.coverImagePath ? (
|
|
<img
|
|
src={getImageUrl(collection.coverImagePath)}
|
|
alt={`${collection.name} cover`}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-100 to-purple-100">
|
|
<div className="text-center">
|
|
<div className="text-sm font-bold theme-text">
|
|
{collection.storyCount}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Collection Details */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold theme-header line-clamp-1">
|
|
{collection.name}
|
|
{collection.isArchived && (
|
|
<span className="ml-2 inline-block bg-yellow-500 text-white px-2 py-1 rounded text-xs">
|
|
Archived
|
|
</span>
|
|
)}
|
|
</h3>
|
|
|
|
{collection.description && (
|
|
<p className="text-sm theme-text opacity-70 line-clamp-2 mt-1">
|
|
{collection.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex items-center gap-4 mt-2 text-sm theme-text opacity-60">
|
|
<span>{collection.storyCount} stories</span>
|
|
<span>{collection.estimatedReadingTime ? formatReadingTime(collection.estimatedReadingTime) : '—'} reading</span>
|
|
{collection.averageStoryRating && collection.averageStoryRating > 0 && (
|
|
<span>★ {collection.averageStoryRating.toFixed(1)} avg</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{collection.tags && collection.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
{collection.tags.slice(0, 5).map((tag) => (
|
|
<span
|
|
key={tag.id}
|
|
className="inline-block px-2 py-1 text-xs rounded-full theme-accent-bg text-white"
|
|
>
|
|
{tag.name}
|
|
</span>
|
|
))}
|
|
{collection.tags.length > 5 && (
|
|
<span className="text-xs theme-text opacity-60">
|
|
+{collection.tags.length - 5} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{collection.rating && (
|
|
<div className="flex-shrink-0 ml-4">
|
|
{renderRatingStars(collection.rating)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
} |