Refactor imports and remove debug logs; add MediaDetailView and MediaListView components with status configuration

This commit is contained in:
Lars Behrends
2026-01-21 22:52:57 +01:00
parent 4853b860fc
commit 68930f15f2
12 changed files with 605 additions and 34 deletions

View File

@@ -154,10 +154,6 @@ export default function Layout({ children }: LayoutProps) {
FiltersComponent: ContentFilters FiltersComponent: ContentFilters
} }
// Debug logs
console.log('Layout.tsx - State:', { viewMode, gridColumns, coverSize, currentView })
console.log('Layout.tsx - viewContextValue:', viewContextValue)
return ( return (
<div className="flex h-screen overflow-hidden relative"> <div className="flex h-screen overflow-hidden relative">
{/* Mobile Menu Overlay */} {/* Mobile Menu Overlay */}
@@ -275,7 +271,6 @@ export default function Layout({ children }: LayoutProps) {
value={gridColumns} value={gridColumns}
onChange={(e) => { onChange={(e) => {
const newColumns = parseInt(e.target.value) const newColumns = parseInt(e.target.value)
console.log('Layout.tsx - Grid slider changed:', newColumns)
setGridColumns(newColumns) setGridColumns(newColumns)
}} }}
className="w-12 md:w-16 h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-brand-500" className="w-12 md:w-16 h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-brand-500"
@@ -296,7 +291,6 @@ export default function Layout({ children }: LayoutProps) {
value={coverSize} value={coverSize}
onChange={(e) => { onChange={(e) => {
const newSize = parseInt(e.target.value) const newSize = parseInt(e.target.value)
console.log('Layout.tsx - Cover slider changed:', newSize)
setCoverSize(newSize) setCoverSize(newSize)
}} }}
className="w-16 md:w-20 h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-brand-500" className="w-16 md:w-20 h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-brand-500"
@@ -310,7 +304,6 @@ export default function Layout({ children }: LayoutProps) {
<div className="flex bg-slate-800/50 p-0.5 md:p-1 rounded-full border border-white/5"> <div className="flex bg-slate-800/50 p-0.5 md:p-1 rounded-full border border-white/5">
<button <button
onClick={() => { onClick={() => {
console.log('Layout.tsx - View mode changed to: list')
setViewMode('list') setViewMode('list')
}} }}
className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'list' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`} className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'list' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`}
@@ -319,7 +312,6 @@ export default function Layout({ children }: LayoutProps) {
</button> </button>
<button <button
onClick={() => { onClick={() => {
console.log('Layout.tsx - View mode changed to: grid')
setViewMode('grid') setViewMode('grid')
}} }}
className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'grid' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`} className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'grid' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`}
@@ -328,7 +320,6 @@ export default function Layout({ children }: LayoutProps) {
</button> </button>
<button <button
onClick={() => { onClick={() => {
console.log('Layout.tsx - View mode changed to: cover')
setViewMode('cover') setViewMode('cover')
}} }}
className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'cover' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`} className={`p-1.5 md:p-2 rounded-full transition-all ${viewMode === 'cover' ? 'bg-white text-brand-900 shadow' : 'text-slate-400 hover:text-white'}`}

View File

@@ -0,0 +1,409 @@
import React, { useMemo } from 'react';
import { ArrowLeft, Calendar, Clock, Trophy, Monitor, Star, Heart, PlayCircle, Building2, Edit2, Sparkles, Layers, Tv, Users, User, Gamepad2, CheckCircle2 } from 'lucide-react';
import { MediaItem } from '../types';
import { STATUS_CONFIG } from '../constants';
interface MediaDetailViewProps {
item: MediaItem | null;
allMedia: MediaItem[];
// Removed isOpen since it is now conditionally rendered by parent
onBack: () => void;
onEdit: (item: MediaItem) => void;
onToggleFavorite: (id: string, isFav: boolean) => void;
onSelectRelated: (item: MediaItem) => void;
onSelectPerson?: (personName: string) => void; // Added prop
}
export const MediaDetailView: React.FC<MediaDetailViewProps> = ({
item,
allMedia,
onBack,
onEdit,
onToggleFavorite,
onSelectRelated,
onSelectPerson
}) => {
const relatedItems = useMemo(() => {
if (!item || !allMedia) return [];
return allMedia
.filter(i => i.id !== item.id) // Exclude itself
.map(candidate => {
let score = 0;
// Same type gives baseline relevance
if (candidate.type === item.type) score += 2;
// Same Studio/Developer (strong signal)
if (item.studio && candidate.studio && item.studio === candidate.studio) score += 3;
// Overlapping Genres
const sharedGenres = candidate.genres.filter(g => item.genres.includes(g)).length;
score += sharedGenres;
return { item: candidate, score };
})
.filter(x => x.score > 0) // Only show if there is some relevance
.sort((a, b) => b.score - a.score)
.slice(0, 5) // Top 5
.map(x => x.item);
}, [item, allMedia]);
if (!item) return null;
const statusConfig = STATUS_CONFIG[item.status];
// Helper to calculate progress percentage for bars
const getProgress = (current: number, total: number) => {
if (!total || total === 0) return 0;
return Math.min(100, Math.round((current / total) * 100));
};
return (
<div className="h-full w-full flex flex-col overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-300 bg-transparent">
{/* Navigation Bar (Page Header) */}
<div className="absolute top-0 left-0 right-0 z-30 p-6 pointer-events-none">
<button
onClick={onBack}
className="pointer-events-auto flex items-center gap-2 px-4 py-2 bg-black/40 hover:bg-black/60 backdrop-blur-md border border-white/10 text-white rounded-full transition-all hover:pl-3 group shadow-lg"
>
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
<span className="font-medium">Zurück</span>
</button>
</div>
<div className="overflow-y-auto custom-scrollbar flex-1 pb-20">
{/* Hero Section */}
<div className="relative h-72 md:h-[450px] w-full shrink-0">
<img
src={item.backdropUrl || item.coverUrl}
alt={item.title}
className="w-full h-full object-cover object-top opacity-60 mask-image-gradient"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark-bg via-dark-bg/40 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-8 md:p-12 flex items-end gap-10 max-w-7xl mx-auto w-full">
{/* Poster Image - Overlapping */}
<img
src={item.coverUrl}
alt={item.title}
className="w-48 h-72 md:w-64 md:h-96 object-cover rounded-xl shadow-2xl border-4 border-dark-bg hidden md:block -mb-20 z-10"
/>
{/* Title & Basic Info */}
<div className="flex-1 min-w-0 pb-4">
<div className="flex items-center gap-3 mb-4">
<span className={`inline-flex items-center space-x-1 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ${statusConfig.color} border border-white/5`}>
{statusConfig.icon}
<span>{statusConfig.label}</span>
</span>
{item.favorite && (
<span className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-red-400 bg-red-500/10 rounded-full border border-red-500/20">
<Heart size={12} fill="currentColor" /> Favorit
</span>
)}
</div>
<h1 className="text-4xl md:text-6xl font-bold text-white leading-tight mb-4 drop-shadow-xl">
{item.title}
</h1>
<div className="flex flex-wrap items-center gap-6 text-sm md:text-base text-slate-300 font-medium">
<div className="flex items-center gap-1.5 text-yellow-400 bg-yellow-400/10 px-2 py-1 rounded">
<Star size={18} fill="currentColor" /> {item.rating}
</div>
<span className="flex items-center gap-1.5"><Calendar size={18} /> {item.releaseYear}</span>
{item.studio && <span className="flex items-center gap-1.5"><Building2 size={18} /> {item.studio}</span>}
{item.type === 'game' && item.platform && <span className="flex items-center gap-1.5"><Monitor size={18} /> {item.platform}</span>}
{item.type === 'adult' && item.platform && <span className="flex items-center gap-1.5 text-purple-400"><Monitor size={18} /> {item.platform}</span>}
</div>
</div>
</div>
</div>
{/* Content Body */}
<div className="p-8 md:p-12 pt-8 md:pt-24 max-w-7xl mx-auto w-full">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Left Column: Description & Actions */}
<div className="lg:col-span-2 space-y-10">
{/* Genres */}
<div className="flex flex-wrap gap-2">
{item.genres.map(g => (
<span key={g} className="px-4 py-2 rounded-full bg-slate-800 text-slate-200 text-sm border border-dark-border font-medium">
{g}
</span>
))}
</div>
{/* Description */}
<div>
<h3 className="text-2xl font-bold text-white mb-4">Beschreibung</h3>
<p className="text-slate-300 leading-relaxed text-lg">
{item.description || "Keine Beschreibung verfügbar."}
</p>
</div>
{/* Cast & Crew Section (Updated to include Adult) */}
{(item.type === 'movie' || item.type === 'series' || item.type === 'adult') && (
<div className="animate-in slide-in-from-bottom-6 duration-500 delay-100">
<h3 className="text-xl font-bold text-white mb-6 flex items-center gap-2">
<Users size={22} className="text-brand-500" />
{item.type === 'adult' ? 'Performer & Besetzung' : 'Besetzung & Crew'}
</h3>
{/* Cast List */}
{item.cast && item.cast.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{item.cast.map((member, idx) => (
<div
key={idx}
onClick={() => onSelectPerson && onSelectPerson(member.name)}
className="flex items-center gap-3 bg-slate-800/50 p-2 rounded-xl border border-white/5 hover:border-brand-500/30 transition-colors group cursor-pointer"
>
<div className="w-10 h-10 rounded-full overflow-hidden bg-slate-700 shrink-0 border border-white/10">
{member.imageUrl ? (
<img src={member.imageUrl} alt={member.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-slate-500">
<User size={16} />
</div>
)}
</div>
<div className="min-w-0">
<div className="text-xs font-bold text-white truncate group-hover:text-brand-400 transition-colors">{member.name}</div>
<div className="text-[10px] text-slate-400 truncate">{member.role}</div>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center p-8 border border-dashed border-white/10 rounded-xl bg-slate-800/20">
<Users size={32} className="text-slate-600 mb-2" />
<p className="text-slate-500 text-sm">Keine Informationen zur Besetzung verfügbar.</p>
</div>
)}
</div>
)}
{/* Actions Bar */}
<div className="flex gap-4 pt-4 border-b border-dark-border pb-10">
<button
onClick={() => onEdit(item)}
className="px-6 py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-lg font-medium transition-colors border border-dark-border flex items-center gap-2"
>
<Edit2 size={18} />
Bearbeiten
</button>
<button
onClick={() => onToggleFavorite(item.id, !item.favorite)}
className={`px-6 py-3 rounded-lg font-medium transition-colors border flex items-center justify-center gap-2 ${
item.favorite
? 'bg-red-500/10 border-red-500/30 text-red-500 hover:bg-red-500/20'
: 'bg-slate-800 border-dark-border hover:bg-slate-700 text-slate-300'
}`}
>
<Heart size={20} fill={item.favorite ? "currentColor" : "none"} />
{item.favorite ? 'Favorisiert' : 'Favorisieren'}
</button>
</div>
</div>
{/* Right Column: Stats & Metadata */}
<div className="space-y-8">
{/* Stats Box */}
<div className="bg-dark-surface p-8 rounded-2xl border border-dark-border space-y-8 shadow-xl">
<h3 className="font-bold text-white flex items-center gap-2 text-lg">
<Trophy size={24} className="text-brand-500" />
Statistiken
</h3>
{/* Game Stats */}
{item.type === 'game' && (
<div className="space-y-6">
{/* Playtime & Completion */}
<div className="bg-slate-800/50 p-6 rounded-xl border border-white/10 relative overflow-hidden">
<div className="relative z-10 flex flex-col gap-6">
{/* Playtime Header */}
<div>
<div className="text-sm text-brand-400 uppercase font-bold mb-1 flex items-center gap-2">
<Clock size={18} /> Spielzeit
</div>
<div className="text-4xl font-extrabold text-white tracking-tight">
{item.playtime || 0} <span className="text-lg font-medium text-slate-500">Std.</span>
</div>
</div>
{/* Completion Progress */}
<div>
<div className="flex justify-between items-end mb-2">
<div className="text-xs text-slate-400 uppercase font-bold flex items-center gap-1">
<CheckCircle2 size={14} className="text-blue-500" /> Fortschritt
</div>
<div className="text-xl font-bold text-white">
{item.completionRate || 0}%
</div>
</div>
<div className="w-full bg-slate-900 h-4 rounded-full overflow-hidden border border-white/5 relative">
<div
className="bg-gradient-to-r from-blue-600 to-blue-400 h-full rounded-full shadow-[0_0_10px_rgba(59,130,246,0.5)] transition-all duration-1000 ease-out"
style={{ width: `${item.completionRate || 0}%` }}
/>
</div>
</div>
</div>
{/* Decorative Icon */}
<Gamepad2 size={96} className="absolute -right-6 -bottom-6 text-white/5 rotate-12" />
</div>
{/* Achievements */}
{(item.achievementsTotal || 0) > 0 && (
<div className="bg-slate-800/30 rounded-xl p-5 border border-white/5">
<div className="flex justify-between items-end mb-3">
<div className="text-xs text-slate-400 uppercase font-bold flex items-center gap-1">
<Trophy size={14} className="text-yellow-500" /> Erfolge
</div>
<div className="text-2xl font-bold text-white">
{Math.round(((item.achievementsUnlocked || 0) / (item.achievementsTotal || 1)) * 100)}%
</div>
</div>
<div className="w-full bg-slate-900 h-4 rounded-full overflow-hidden border border-white/5 relative mb-2">
<div
className="bg-gradient-to-r from-yellow-600 to-yellow-400 h-full rounded-full shadow-[0_0_10px_rgba(234,179,8,0.5)] transition-all duration-1000 ease-out"
style={{ width: `${getProgress(item.achievementsUnlocked || 0, item.achievementsTotal || 1)}%` }}
/>
</div>
<div className="flex justify-between text-xs text-slate-500 font-medium">
<span>{item.achievementsUnlocked} freigeschaltet</span>
<span>{item.achievementsTotal} gesamt</span>
</div>
</div>
)}
</div>
)}
{/* Series Stats */}
{item.type === 'series' && (
<div className="space-y-6">
{/* Enhanced Season Display */}
<div className="bg-slate-800/50 p-6 rounded-xl border border-white/10 flex items-center justify-between relative overflow-hidden">
<div className="relative z-10">
<div className="text-sm text-brand-400 uppercase font-bold mb-1 flex items-center gap-2">
<Layers size={18} /> Anzahl Staffeln
</div>
<div className="text-4xl font-extrabold text-white tracking-tight">{item.seasons || 1}</div>
</div>
{/* Decorative Icon */}
<Layers size={64} className="absolute -right-4 -bottom-4 text-white/5 rotate-12" />
<div className="h-12 w-1 bg-white/10 rounded-full mx-4"></div>
<div className="text-right relative z-10">
<div className="text-xs text-slate-400 uppercase font-bold mb-1">Gesamt Episoden</div>
<div className="text-2xl font-bold text-white">{item.episodesTotal || '?'}</div>
</div>
</div>
<div className="bg-slate-800/30 rounded-xl p-5 border border-white/5">
<div className="flex justify-between items-end mb-4">
<div className="text-xs text-slate-400 uppercase font-bold flex items-center gap-1">
<PlayCircle size={14} className="text-green-500" /> Episoden-Fortschritt
</div>
<div className="text-3xl font-bold text-white">
{Math.round(((item.episodesWatched || 0) / (item.episodesTotal || 1)) * 100)}%
</div>
</div>
<div className="w-full bg-slate-900 h-5 rounded-full overflow-hidden border border-white/5 relative mb-3">
<div
className="bg-gradient-to-r from-green-600 to-green-400 h-full rounded-full shadow-[0_0_15px_rgba(74,222,128,0.4)] transition-all duration-1000 ease-out"
style={{ width: `${getProgress(item.episodesWatched || 0, item.episodesTotal || 1)}%` }}
/>
</div>
<div className="flex justify-between items-center text-sm">
<div className="flex flex-col">
<span className="text-slate-400 text-xs">Bereits gesehen</span>
<span className="text-white font-bold">{item.episodesWatched || 0} <span className="text-slate-500 font-normal">Episoden</span></span>
</div>
<div className="flex flex-col items-end">
<span className="text-slate-400 text-xs">Verfügbar</span>
<span className="text-white font-bold">{item.episodesTotal || '?'} <span className="text-slate-500 font-normal">Episoden</span></span>
</div>
</div>
</div>
</div>
)}
{/* Movie & Adult Stats (Duration) */}
{(item.type === 'movie' || item.type === 'adult') && (
<div>
<div className="text-xs text-slate-400 uppercase font-bold mb-1">Laufzeit</div>
<div className="text-3xl font-bold text-white">{item.duration} <span className="text-base font-normal text-slate-500">Minuten</span></div>
</div>
)}
</div>
{/* Additional Metadata Info Box */}
<div className="bg-dark-surface/50 p-6 rounded-2xl border border-dark-border">
<div className="space-y-3 text-sm">
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-slate-400">Hinzugefügt am</span>
<span className="text-slate-200">{new Date(item.addedAt).toLocaleDateString()}</span>
</div>
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-slate-400">Typ</span>
<span className="text-slate-200 capitalize">{item.type}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-slate-400">ID</span>
<span className="text-slate-500 font-mono text-xs">{item.id.substring(0,8)}...</span>
</div>
</div>
</div>
</div>
</div>
{/* Related Media Section */}
{relatedItems.length > 0 && (
<div className="mt-16 mb-8 animate-in slide-in-from-bottom-5 duration-300">
<h3 className="text-2xl font-bold text-white mb-6 flex items-center gap-2">
<Sparkles size={24} className="text-brand-500" />
Ähnliche Titel
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6">
{relatedItems.map(related => (
<div
key={related.id}
onClick={() => onSelectRelated(related)}
className="group cursor-pointer bg-dark-surface rounded-xl overflow-hidden border border-dark-border hover:border-brand-500/50 hover:shadow-lg transition-all"
>
<div className="aspect-[2/3] overflow-hidden relative">
<img
src={related.coverUrl}
alt={related.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
<div className="absolute top-2 right-2 px-1.5 py-0.5 bg-black/60 backdrop-blur-md rounded text-[10px] text-white flex items-center gap-1 font-bold">
<Star size={10} className="text-yellow-400 fill-current" />
{related.rating}
</div>
</div>
<div className="p-4">
<h4 className="font-bold text-white text-sm truncate group-hover:text-brand-400 transition-colors">{related.title}</h4>
<p className="text-xs text-slate-500">{related.releaseYear}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,129 @@
import React from 'react';
import { MediaItem } from '../types';
import { Monitor, Star, Check, Circle, Clock, CheckCircle2, Gamepad2, Film, Tv } from 'lucide-react';
import { STATUS_CONFIG } from '../constants';
interface MediaListViewProps {
items: MediaItem[];
onSelect: (item: MediaItem) => void;
selectedId: string | null;
onToggleSelect: (id: string) => void;
multiSelectedIds: Set<string>;
}
export const MediaListView: React.FC<MediaListViewProps> = ({
items,
onSelect,
selectedId,
onToggleSelect,
multiSelectedIds
}) => {
const getTypeIcon = (type: string) => {
switch(type) {
case 'game': return <Gamepad2 size={14} className="text-brand-400" />;
case 'movie': return <Film size={14} className="text-purple-400" />;
case 'series': return <Tv size={14} className="text-pink-400" />;
default: return <Circle size={14} />;
}
};
const getStatusIcon = (status: string) => {
const config = STATUS_CONFIG[status as keyof typeof STATUS_CONFIG];
const iconClass = `${config.color.replace('bg-', 'text-').replace('/10', '')}`;
switch(config.icon) {
case 'check-circle': return <CheckCircle2 className={`w-4 h-4 ${iconClass}`} />;
case 'monitor': return <Monitor className={`w-4 h-4 ${iconClass}`} />;
case 'clock': return <Clock className={`w-4 h-4 ${iconClass}`} />;
case 'circle': return <Circle className={`w-4 h-4 ${iconClass}`} />;
default: return <Circle className={`w-4 h-4 ${iconClass}`} />;
}
};
return (
<div className="w-full h-full bg-white text-slate-900 dark:bg-[#1e293b] dark:text-slate-200 text-sm overflow-hidden flex flex-col border-r border-dark-border">
{/* Table Header */}
<div className="flex items-center bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 font-bold text-xs uppercase tracking-wide text-slate-500 h-8 shrink-0 select-none">
<div className="w-10 flex justify-center">{/* Checkbox */}</div>
<div className="w-10 flex justify-center">Type</div>
<div className="flex-1 px-3 border-l border-slate-200 dark:border-slate-700/50">Title</div>
<div className="w-32 px-3 border-l border-slate-200 dark:border-slate-700/50 hidden md:block">Platform</div>
<div className="w-20 px-3 border-l border-slate-200 dark:border-slate-700/50 text-center">Year</div>
<div className="w-20 px-3 border-l border-slate-200 dark:border-slate-700/50 text-center">Score</div>
<div className="w-10 flex justify-center border-l border-slate-200 dark:border-slate-700/50">Stat</div>
</div>
{/* Table Body */}
<div className="flex-1 overflow-y-auto custom-scrollbar">
{items.length === 0 ? (
<div className="p-8 text-center text-slate-500">No items found</div>
) : (
items.map((item, index) => {
const isRowSelected = selectedId === item.id;
const isChecked = multiSelectedIds.has(item.id);
return (
<div
key={item.id}
onClick={() => onSelect(item)}
className={`flex items-center h-9 border-b border-slate-100 dark:border-slate-700/50 cursor-pointer transition-colors ${
isRowSelected
? 'bg-blue-500 text-white'
: index % 2 === 0 ? 'bg-slate-50 dark:bg-slate-800/30 hover:bg-blue-50 dark:hover:bg-slate-700' : 'bg-white dark:bg-transparent hover:bg-blue-50 dark:hover:bg-slate-700'
}`}
>
{/* Checkbox */}
<div className="w-10 flex justify-center shrink-0" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={isChecked}
onChange={() => onToggleSelect(item.id)}
className={`rounded border-slate-300 dark:border-slate-600 ${isRowSelected ? 'text-white border-white' : 'text-blue-500'}`}
/>
</div>
{/* Icon */}
<div className={`w-10 flex justify-center shrink-0 ${isRowSelected ? 'text-white' : ''}`}>
{getTypeIcon(item.type)}
</div>
{/* Title */}
<div className="flex-1 px-3 truncate font-medium">
{item.title}
</div>
{/* Platform (Hidden on mobile) */}
<div className={`w-32 px-3 truncate hidden md:block text-xs ${isRowSelected ? 'text-blue-100' : 'text-slate-500'}`}>
{item.platform || item.type}
</div>
{/* Year */}
<div className={`w-20 px-3 text-center text-xs ${isRowSelected ? 'text-blue-100' : 'text-slate-500'}`}>
{item.releaseYear}
</div>
{/* Rating */}
<div className={`w-20 px-3 text-center text-xs font-mono ${isRowSelected ? 'text-blue-100' : 'text-slate-500'}`}>
{item.rating > 0 ? item.rating.toFixed(1) : '-'}
</div>
{/* Status Icon */}
<div className={`w-10 flex justify-center shrink-0 ${isRowSelected ? 'text-white' : ''}`}>
{getStatusIcon(item.status)}
</div>
</div>
);
})
)}
</div>
{/* Footer Status Bar matching screenshot */}
<div className="h-6 bg-slate-200 dark:bg-slate-900 border-t border-slate-300 dark:border-slate-800 flex items-center px-2 text-[10px] text-slate-500 select-none">
<span>{items.length} Items listed</span>
<div className="mx-2 h-3 w-px bg-slate-400 dark:bg-slate-700"></div>
<span>{multiSelectedIds.size} Selected</span>
</div>
</div>
);
};

27
src/constants/index.ts Normal file
View File

@@ -0,0 +1,27 @@
export const STATUS_CONFIG = {
completed: {
label: 'Completed',
color: 'bg-green-500/10',
icon: 'check-circle'
},
watching: {
label: 'Watching',
color: 'bg-blue-500/10',
icon: 'monitor'
},
plan_to_watch: {
label: 'Plan to Watch',
color: 'bg-yellow-500/10',
icon: 'clock'
},
on_hold: {
label: 'On Hold',
color: 'bg-orange-500/10',
icon: 'circle'
},
dropped: {
label: 'Dropped',
color: 'bg-red-500/10',
icon: 'circle'
}
} as const;

View File

@@ -16,8 +16,8 @@ import { useAdults } from '../hooks/useApi'
import { Tooltip } from '../components/MicroInteractions' import { Tooltip } from '../components/MicroInteractions'
import { ViewContext } from '../components/Layout' import { ViewContext } from '../components/Layout'
// Import from alternative frontend // Import from components
import { MediaListView } from '../../../frontend/components/MediaListView' import { MediaListView } from '../components/MediaListView'
interface PaginatedResponse<T> { interface PaginatedResponse<T> {
items: T[] items: T[]

View File

@@ -16,9 +16,9 @@ import { useAdults } from '../hooks/useApi'
import { Tooltip } from '../components/MicroInteractions' import { Tooltip } from '../components/MicroInteractions'
import { ViewContext } from '../components/Layout' import { ViewContext } from '../components/Layout'
// Import from alternative frontend // Import from components
import { MediaListView } from '../../../frontend/components/MediaListView' import { MediaListView } from '../components/MediaListView'
import { MediaDetailView } from '../../../frontend/components/MediaDetailView' import { MediaDetailView } from '../components/MediaDetailView'
interface PaginatedResponse<T> { interface PaginatedResponse<T> {
items: T[] items: T[]

View File

@@ -5,9 +5,9 @@ import { ViewContext } from '../components/Layout'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ComputerDesktopIcon as GamepadIcon } from '@heroicons/react/24/outline' import { ComputerDesktopIcon as GamepadIcon } from '@heroicons/react/24/outline'
// Import from alternative frontend // Import from components
import { MediaListView } from '../../../frontend/components/MediaListView' import { MediaListView } from '../components/MediaListView'
import { MediaDetailView } from '../../../frontend/components/MediaDetailView' import { MediaDetailView } from '../components/MediaDetailView'
export default function Games() { export default function Games() {
const viewContext = useContext(ViewContext) const viewContext = useContext(ViewContext)

View File

@@ -6,9 +6,9 @@ import { useMovies } from '../hooks/useApi'
import { ViewContext } from '../components/Layout' import { ViewContext } from '../components/Layout'
import { FilmIcon } from '@heroicons/react/24/outline' import { FilmIcon } from '@heroicons/react/24/outline'
// Import from alternative frontend // Import from components
import { MediaListView } from '../../../frontend/components/MediaListView' import { MediaListView } from '../components/MediaListView'
import { MediaDetailView } from '../../../frontend/components/MediaDetailView' import { MediaDetailView } from '../components/MediaDetailView'
export default function Movies() { export default function Movies() {
const viewContext = useContext(ViewContext) const viewContext = useContext(ViewContext)

View File

@@ -6,9 +6,9 @@ import { TvIcon } from '@heroicons/react/24/outline'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Play, Eye, Lock} from 'lucide-react' import { Play, Eye, Lock} from 'lucide-react'
// Import from alternative frontend // Import from components
import { MediaListView } from '../../../frontend/components/MediaListView' import { MediaListView } from '../components/MediaListView'
import { MediaDetailView } from '../../../frontend/components/MediaDetailView' import { MediaDetailView } from '../components/MediaDetailView'
export default function TVShows() { export default function TVShows() {
const viewContext = useContext(ViewContext) const viewContext = useContext(ViewContext)

View File

@@ -4,14 +4,6 @@ import { PaginatedResponse } from '../types'
// API base configuration // API base configuration
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || '/api' const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || '/api'
// Types for API responses
interface ApiResponse<T = any> {
success: boolean
data?: T
message?: string
error?: string
}
interface LocalPaginatedResponse<T> { interface LocalPaginatedResponse<T> {
items: T[] items: T[]
pagination: { pagination: {
@@ -228,6 +220,10 @@ export const tvShowsApi = {
// Return fallback data if API fails // Return fallback data if API fails
return { return {
items: [], items: [],
total: 0,
page: 1,
limit: 20,
totalPages: 1,
pagination: { pagination: {
total: 0, total: 0,
per_page: 20, per_page: 20,
@@ -512,6 +508,10 @@ export const adultApi = {
// Return fallback data if API fails // Return fallback data if API fails
return { return {
items: [], items: [],
total: 0,
page: 1,
limit: 20,
totalPages: 1,
pagination: { pagination: {
total: 0, total: 0,
per_page: 20, per_page: 20,
@@ -578,6 +578,10 @@ export const actorsApi = {
// Return fallback data if API fails // Return fallback data if API fails
return { return {
items: [], items: [],
total: 0,
page: 1,
limit: 20,
totalPages: 1,
pagination: { pagination: {
total: 0, total: 0,
per_page: 20, per_page: 20,

View File

@@ -72,11 +72,22 @@ export interface SearchParams {
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
data: T[] items: T[]
total: number total: number
page: number page: number
limit: number limit: number
totalPages: number totalPages: number
pagination?: {
total: number
per_page: number
current_page: number
last_page: number
}
available_filters?: {
genres?: string[]
directors?: string[]
sources?: string[]
}
} }
export type ViewMode = 'grid' | 'list' | 'covers' export type ViewMode = 'grid' | 'list' | 'covers' | 'cover'

View File

@@ -15,7 +15,7 @@ export default defineConfig({
} }
}, },
build: { build: {
outDir: '../public/react', outDir: '../public/dist',
emptyOutDir: true emptyOutDir: true
} }
}) })