178 lines
5.8 KiB
TypeScript
178 lines
5.8 KiB
TypeScript
import { motion } from 'framer-motion';
|
|
import { Film, Tv, Music, Calendar, Check, Clock } from 'lucide-react';
|
|
import type { CalendarItem } from '../../types/unified';
|
|
import Badge from '../ui/Badge';
|
|
|
|
interface UpcomingListProps {
|
|
items: CalendarItem[];
|
|
isLoading?: boolean;
|
|
onItemClick?: (item: CalendarItem) => void;
|
|
}
|
|
|
|
export default function UpcomingList({
|
|
items,
|
|
isLoading = false,
|
|
onItemClick,
|
|
}: UpcomingListProps) {
|
|
const getTypeIcon = (type: CalendarItem['type']) => {
|
|
switch (type) {
|
|
case 'movie':
|
|
return <Film size={16} className="text-red-400" />;
|
|
case 'episode':
|
|
return <Tv size={16} className="text-blue-400" />;
|
|
case 'album':
|
|
return <Music size={16} className="text-purple-400" />;
|
|
}
|
|
};
|
|
|
|
const getTypeBadge = (type: CalendarItem['type']) => {
|
|
switch (type) {
|
|
case 'movie':
|
|
return <Badge size="sm" className="bg-red-500/20 text-red-400">Movie</Badge>;
|
|
case 'episode':
|
|
return <Badge size="sm" className="bg-blue-500/20 text-blue-400">Episode</Badge>;
|
|
case 'album':
|
|
return <Badge size="sm" className="bg-purple-500/20 text-purple-400">Album</Badge>;
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
const now = new Date();
|
|
const diff = date.getTime() - now.getTime();
|
|
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days === 0) return 'Today';
|
|
if (days === 1) return 'Tomorrow';
|
|
if (days < 7) return `In ${days} days`;
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
|
|
});
|
|
};
|
|
|
|
// Group items by date
|
|
const groupedItems = items.reduce((acc, item) => {
|
|
const dateKey = item.date.toDateString();
|
|
if (!acc[dateKey]) {
|
|
acc[dateKey] = { date: item.date, items: [] };
|
|
}
|
|
acc[dateKey].items.push(item);
|
|
return acc;
|
|
}, {} as Record<string, { date: Date; items: CalendarItem[] }>);
|
|
|
|
const sortedGroups = Object.values(groupedItems).sort(
|
|
(a, b) => a.date.getTime() - b.date.getTime()
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="glass rounded-lg p-4 animate-pulse">
|
|
<div className="h-4 bg-white/10 rounded w-24 mb-3" />
|
|
<div className="space-y-2">
|
|
<div className="h-16 bg-white/5 rounded" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="glass rounded-lg p-8 text-center">
|
|
<Calendar size={48} className="mx-auto text-gray-500 mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-300">No upcoming releases</h3>
|
|
<p className="text-gray-500 mt-1">Check back later for new content</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{sortedGroups.map(({ date, items: dateItems }) => (
|
|
<div key={date.toDateString()}>
|
|
{/* Date Header */}
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Calendar size={16} className="text-gray-400" />
|
|
<h3 className="font-medium text-gray-300">
|
|
{formatDate(date)}
|
|
</h3>
|
|
<span className="text-sm text-gray-500">
|
|
{date.toLocaleDateString('en-US', { weekday: 'long' })}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Items */}
|
|
<div className="space-y-2">
|
|
{dateItems.map((item) => (
|
|
<motion.div
|
|
key={item.id}
|
|
whileHover={{ scale: 1.01, x: 4 }}
|
|
onClick={() => onItemClick?.(item)}
|
|
className="glass rounded-lg p-4 flex items-center gap-4 cursor-pointer hover:bg-white/5 transition-colors"
|
|
>
|
|
{/* Poster */}
|
|
{item.poster ? (
|
|
<img
|
|
src={item.poster}
|
|
alt={item.title}
|
|
className="w-12 h-16 object-cover rounded"
|
|
/>
|
|
) : (
|
|
<div className="w-12 h-16 rounded bg-white/10 flex items-center justify-center">
|
|
{getTypeIcon(item.type)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
{getTypeIcon(item.type)}
|
|
<h4 className="font-medium text-white line-clamp-1">
|
|
{item.title}
|
|
</h4>
|
|
</div>
|
|
{item.subtitle && (
|
|
<p className="text-sm text-gray-400 line-clamp-1">
|
|
{item.subtitle}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-2">
|
|
{getTypeBadge(item.type)}
|
|
<span className="text-xs text-gray-500 flex items-center gap-1">
|
|
<Clock size={10} />
|
|
{date.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit'
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div>
|
|
{item.hasFile ? (
|
|
<Badge variant="success" className="flex items-center gap-1">
|
|
<Check size={12} />
|
|
Available
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="info" className="flex items-center gap-1">
|
|
<Clock size={12} />
|
|
Upcoming
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|