Introduce ThemeContext and apply theme tokens

Add a ThemeContext and provider, wrap the app with ThemeProvider, and sync user settings' theme into the context. Replace hardcoded color classes with design token classes (background, muted, foreground, border, card, etc.) across multiple UI components to centralize theming and enable consistent light/dark styling. Files updated include App.tsx (useTheme, setTheme, ThemeProvider, footer/background tokens), several views and components (AddMediaView, BrowseView, CastDetailView, CastView, MediaCard, MediaListItem, SettingsView, ImporterView) to use tokenized classes, and add new src/contexts/ThemeContext.tsx.
This commit is contained in:
Lars Behrends
2026-04-10 14:59:40 +02:00
parent 96593a6235
commit b29732a653
11 changed files with 392 additions and 304 deletions

View File

@@ -17,11 +17,13 @@ import SettingsView from './components/SettingsView';
import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data';
import { Media, Staff, MediaCategory, UserSettings } from './types'; import { Media, Staff, MediaCategory, UserSettings } from './types';
import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api'; import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api';
import { ThemeProvider, useTheme } from './contexts/ThemeContext';
function AppContent() { function AppContent() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { setTheme } = useTheme();
const [activeCategory, setActiveCategory] = useState<MediaCategory>( const [activeCategory, setActiveCategory] = useState<MediaCategory>(
(searchParams.get('category') as MediaCategory) || 'Anime' (searchParams.get('category') as MediaCategory) || 'Anime'
); );
@@ -43,6 +45,8 @@ function AppContent() {
if (loadedSettings) { if (loadedSettings) {
setSettings(loadedSettings); setSettings(loadedSettings);
setEnabledCategories(loadedSettings.enabledCategories); setEnabledCategories(loadedSettings.enabledCategories);
// Sync theme with theme context
setTheme(loadedSettings.theme);
} }
} catch (error) { } catch (error) {
console.error('Failed to load settings from API:', error); console.error('Failed to load settings from API:', error);
@@ -50,7 +54,7 @@ function AppContent() {
}; };
loadSettingsFromApi(); loadSettingsFromApi();
}, []); }, [setTheme]);
const reloadSettings = async () => { const reloadSettings = async () => {
try { try {
@@ -58,6 +62,8 @@ function AppContent() {
if (loadedSettings) { if (loadedSettings) {
setSettings(loadedSettings); setSettings(loadedSettings);
setEnabledCategories(loadedSettings.enabledCategories); setEnabledCategories(loadedSettings.enabledCategories);
// Sync theme with theme context
setTheme(loadedSettings.theme);
} }
} catch (error) { } catch (error) {
console.error('Failed to reload settings from API:', error); console.error('Failed to reload settings from API:', error);
@@ -270,7 +276,7 @@ function AppContent() {
}; };
return ( return (
<div className="min-h-screen bg-white font-sans selection:bg-[#6d28d9]/20 selection:text-[#6d28d9]"> <div className="min-h-screen bg-background font-sans selection:bg-[#6d28d9]/20 selection:text-[#6d28d9]">
<Header <Header
onSearch={handleSearch} onSearch={handleSearch}
activeCategory={activeCategory} activeCategory={activeCategory}
@@ -315,6 +321,7 @@ function AppContent() {
<Route path="/add" element={ <Route path="/add" element={
<AddMediaView <AddMediaView
activeCategory={activeCategory} activeCategory={activeCategory}
enabledCategories={enabledCategories}
onAddComplete={handleAddMedia} onAddComplete={handleAddMedia}
/> />
} /> } />
@@ -329,18 +336,18 @@ function AppContent() {
</main> </main>
{/* Footer */} {/* Footer */}
<footer className="py-12 px-6 border-t border-zinc-100 bg-zinc-50"> <footer className="py-12 px-6 border-t border-border bg-muted/50">
<div className="max-w-[1600px] mx-auto flex flex-col md:flex-row items-center justify-between gap-6"> <div className="max-w-[1600px] mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-2 text-xl font-black text-zinc-400"> <div className="flex items-center gap-2 text-xl font-black text-muted-foreground">
<div className="w-5 h-5 bg-zinc-300 rounded-full" /> <div className="w-5 h-5 bg-muted rounded-full" />
kyoo kyoo
</div> </div>
<div className="flex items-center gap-8 text-sm font-bold text-zinc-400"> <div className="flex items-center gap-8 text-sm font-bold text-muted-foreground">
<a href="#" className="hover:text-[#6d28d9] transition-colors">Terms</a> <a href="#" className="hover:text-[#6d28d9] transition-colors">Terms</a>
<a href="#" className="hover:text-[#6d28d9] transition-colors">Privacy</a> <a href="#" className="hover:text-[#6d28d9] transition-colors">Privacy</a>
<a href="#" className="hover:text-[#6d28d9] transition-colors">Contact</a> <a href="#" className="hover:text-[#6d28d9] transition-colors">Contact</a>
</div> </div>
<p className="text-xs font-medium text-zinc-400"> <p className="text-xs font-medium text-muted-foreground">
© 2026 Kyoo Media Discovery. All rights reserved. © 2026 Kyoo Media Discovery. All rights reserved.
</p> </p>
</div> </div>
@@ -428,7 +435,9 @@ function CastDetailRoute({ selectedPerson, setSelectedPerson }: any) {
export default function App() { export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<ThemeProvider>
<AppContent /> <AppContent />
</ThemeProvider>
</BrowserRouter> </BrowserRouter>
); );
} }

View File

@@ -10,10 +10,11 @@ import { cn } from '@/lib/utils';
interface AddMediaViewProps { interface AddMediaViewProps {
activeCategory: MediaCategory; activeCategory: MediaCategory;
enabledCategories: MediaCategory[];
onAddComplete: () => void; onAddComplete: () => void;
} }
export default function AddMediaView({ activeCategory, onAddComplete }: AddMediaViewProps) { export default function AddMediaView({ activeCategory, enabledCategories, onAddComplete }: AddMediaViewProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [newMedia, setNewMedia] = useState({ const [newMedia, setNewMedia] = useState({
title: '', title: '',
@@ -151,62 +152,62 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
<Button <Button
variant="ghost" variant="ghost"
onClick={() => navigate('/')} onClick={() => navigate('/')}
className="mb-6 gap-2 text-zinc-600 hover:text-zinc-900" className="mb-6 gap-2 text-muted-foreground hover:text-foreground"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} />
Back to Browse Back to Browse
</Button> </Button>
<div className="bg-white rounded-3xl shadow-xl p-8"> <div className="bg-card rounded-3xl shadow-xl p-8 border border-border">
<h1 className="text-3xl font-black text-zinc-900 mb-2">Add New Media</h1> <h1 className="text-3xl font-black text-foreground mb-2">Add New Media</h1>
<p className="text-zinc-500 font-medium mb-8"> <p className="text-muted-foreground font-medium mb-8">
Add a new item to your {activeCategory} library. Add a new item to your {activeCategory} library.
</p> </p>
{submitStatus === 'success' && ( {submitStatus === 'success' && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl"> <div className="mb-6 p-4 bg-green-500/10 border border-green-500/30 rounded-xl">
<p className="text-green-800 font-bold"> Successfully added to library!</p> <p className="text-green-500 font-bold"> Successfully added to library!</p>
</div> </div>
)} )}
{submitStatus === 'error' && ( {submitStatus === 'error' && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl"> <div className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-xl">
<p className="text-red-800 font-bold"> Error: {errorMessage}</p> <p className="text-red-500 font-bold"> Error: {errorMessage}</p>
</div> </div>
)} )}
<form onSubmit={handleAddSubmit} className="space-y-6"> <form onSubmit={handleAddSubmit} className="space-y-6">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="title" className="text-sm font-black text-zinc-700">Title</Label> <Label htmlFor="title" className="text-sm font-black text-foreground">Title</Label>
<Input <Input
id="title" id="title"
value={newMedia.title} value={newMedia.title}
onChange={e => setNewMedia(prev => ({ ...prev, title: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, title: e.target.value }))}
placeholder="e.g. Mob Psycho 100" placeholder="e.g. Mob Psycho 100"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
required required
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="year" className="text-sm font-black text-zinc-700">Year</Label> <Label htmlFor="year" className="text-sm font-black text-foreground">Year</Label>
<Input <Input
id="year" id="year"
value={newMedia.year} value={newMedia.year}
onChange={e => setNewMedia(prev => ({ ...prev, year: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, year: e.target.value }))}
placeholder="2024" placeholder="2024"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="category" className="text-sm font-black text-zinc-700">Category</Label> <Label htmlFor="category" className="text-sm font-black text-foreground">Category</Label>
<select <select
id="category" id="category"
value={newMedia.category} value={newMedia.category}
onChange={e => setNewMedia(prev => ({ ...prev, category: e.target.value as MediaCategory }))} onChange={e => setNewMedia(prev => ({ ...prev, category: e.target.value as MediaCategory }))}
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-background border-border rounded-xl h-11 px-3 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'].map(cat => ( {enabledCategories.map(cat => (
<option key={cat} value={cat}>{cat}</option> <option key={cat} value={cat}>{cat}</option>
))} ))}
</select> </select>
@@ -214,12 +215,12 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="type" className="text-sm font-black text-zinc-700">Type</Label> <Label htmlFor="type" className="text-sm font-black text-foreground">Type</Label>
<select <select
id="type" id="type"
value={newMedia.type} value={newMedia.type}
onChange={e => setNewMedia(prev => ({ ...prev, type: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, type: e.target.value }))}
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-background border-border rounded-xl h-11 px-3 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{newMedia.category === 'Music' ? ( {newMedia.category === 'Music' ? (
<> <>
@@ -253,12 +254,12 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
</select> </select>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="status" className="text-sm font-black text-zinc-700">Status</Label> <Label htmlFor="status" className="text-sm font-black text-foreground">Status</Label>
<select <select
id="status" id="status"
value={newMedia.status} value={newMedia.status}
onChange={e => setNewMedia(prev => ({ ...prev, status: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, status: e.target.value }))}
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-background border-border rounded-xl h-11 px-3 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
<option value="Released">Released</option> <option value="Released">Released</option>
<option value="Ongoing">Ongoing</option> <option value="Ongoing">Ongoing</option>
@@ -274,12 +275,12 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
</div> </div>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="aspectRatio" className="text-sm font-black text-zinc-700">Aspect Ratio (Format)</Label> <Label htmlFor="aspectRatio" className="text-sm font-black text-foreground">Aspect Ratio (Format)</Label>
<select <select
id="aspectRatio" id="aspectRatio"
value={newMedia.aspectRatio} value={newMedia.aspectRatio}
onChange={e => setNewMedia(prev => ({ ...prev, aspectRatio: e.target.value as '2/3' | '16/9' | '1/1' }))} onChange={e => setNewMedia(prev => ({ ...prev, aspectRatio: e.target.value as '2/3' | '16/9' | '1/1' }))}
className="bg-zinc-50 border border-zinc-100 rounded-xl h-11 px-3 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-background border-border rounded-xl h-11 px-3 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
<option value="2/3">2:3 (Standard Poster)</option> <option value="2/3">2:3 (Standard Poster)</option>
<option value="16/9">16:9 (Wide Thumbnail)</option> <option value="16/9">16:9 (Wide Thumbnail)</option>
@@ -287,38 +288,38 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
</select> </select>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="poster" className="text-sm font-black text-zinc-700">Poster URL</Label> <Label htmlFor="poster" className="text-sm font-black text-foreground">Poster URL</Label>
<Input <Input
id="poster" id="poster"
value={newMedia.poster} value={newMedia.poster}
onChange={e => setNewMedia(prev => ({ ...prev, poster: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, poster: e.target.value }))}
placeholder="https://example.com/poster.jpg" placeholder="https://example.com/poster.jpg"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
required required
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="banner" className="text-sm font-black text-zinc-700">Banner URL (Optional)</Label> <Label htmlFor="banner" className="text-sm font-black text-foreground">Banner URL (Optional)</Label>
<Input <Input
id="banner" id="banner"
value={newMedia.banner} value={newMedia.banner}
onChange={e => setNewMedia(prev => ({ ...prev, banner: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, banner: e.target.value }))}
placeholder="https://example.com/banner.jpg" placeholder="https://example.com/banner.jpg"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="description" className="text-sm font-black text-zinc-700">Description (Optional)</Label> <Label htmlFor="description" className="text-sm font-black text-foreground">Description (Optional)</Label>
<textarea <textarea
id="description" id="description"
value={newMedia.description} value={newMedia.description}
onChange={e => setNewMedia(prev => ({ ...prev, description: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, description: e.target.value }))}
placeholder="Brief description..." placeholder="Brief description..."
className="bg-zinc-50 border-zinc-100 rounded-xl p-3 h-20 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none resize-none" className="bg-muted border-border rounded-xl p-3 h-20 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none resize-none"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="rating" className="text-sm font-black text-zinc-700">Rating (Optional)</Label> <Label htmlFor="rating" className="text-sm font-black text-foreground">Rating (Optional)</Label>
<Input <Input
id="rating" id="rating"
type="number" type="number"
@@ -328,84 +329,84 @@ export default function AddMediaView({ activeCategory, onAddComplete }: AddMedia
value={newMedia.rating} value={newMedia.rating}
onChange={e => setNewMedia(prev => ({ ...prev, rating: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, rating: e.target.value }))}
placeholder="8.5" placeholder="8.5"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
{(newMedia.category === 'Anime' || newMedia.category === 'Movies' || newMedia.category === 'TV Series' || newMedia.category === 'Adult') && ( {(newMedia.category === 'Anime' || newMedia.category === 'Movies' || newMedia.category === 'TV Series' || newMedia.category === 'Adult') && (
<> <>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="runtime" className="text-sm font-black text-zinc-700">Runtime (min)</Label> <Label htmlFor="runtime" className="text-sm font-black text-foreground">Runtime (min)</Label>
<Input <Input
id="runtime" id="runtime"
type="number" type="number"
value={newMedia.runtime} value={newMedia.runtime}
onChange={e => setNewMedia(prev => ({ ...prev, runtime: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, runtime: e.target.value }))}
placeholder="120" placeholder="120"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="releaseDate" className="text-sm font-black text-zinc-700">Release Date</Label> <Label htmlFor="releaseDate" className="text-sm font-black text-foreground">Release Date</Label>
<Input <Input
id="releaseDate" id="releaseDate"
type="date" type="date"
value={newMedia.releaseDate} value={newMedia.releaseDate}
onChange={e => setNewMedia(prev => ({ ...prev, releaseDate: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, releaseDate: e.target.value }))}
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="director" className="text-sm font-black text-zinc-700">Director</Label> <Label htmlFor="director" className="text-sm font-black text-foreground">Director</Label>
<Input <Input
id="director" id="director"
value={newMedia.director} value={newMedia.director}
onChange={e => setNewMedia(prev => ({ ...prev, director: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, director: e.target.value }))}
placeholder="Director name" placeholder="Director name"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="writer" className="text-sm font-black text-zinc-700">Writer</Label> <Label htmlFor="writer" className="text-sm font-black text-foreground">Writer</Label>
<Input <Input
id="writer" id="writer"
value={newMedia.writer} value={newMedia.writer}
onChange={e => setNewMedia(prev => ({ ...prev, writer: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, writer: e.target.value }))}
placeholder="Writer name" placeholder="Writer name"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
</> </>
)} )}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="genres" className="text-sm font-black text-zinc-700">Genres (comma-separated)</Label> <Label htmlFor="genres" className="text-sm font-black text-foreground">Genres (comma-separated)</Label>
<Input <Input
id="genres" id="genres"
value={newMedia.genres} value={newMedia.genres}
onChange={e => setNewMedia(prev => ({ ...prev, genres: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, genres: e.target.value }))}
placeholder="Action, Drama, Sci-Fi" placeholder="Action, Drama, Sci-Fi"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="tags" className="text-sm font-black text-zinc-700">Tags (comma-separated)</Label> <Label htmlFor="tags" className="text-sm font-black text-foreground">Tags (comma-separated)</Label>
<Input <Input
id="tags" id="tags"
value={newMedia.tags} value={newMedia.tags}
onChange={e => setNewMedia(prev => ({ ...prev, tags: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, tags: e.target.value }))}
placeholder="Classic, Best-selling" placeholder="Classic, Best-selling"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="studios" className="text-sm font-black text-zinc-700">Studios (comma-separated)</Label> <Label htmlFor="studios" className="text-sm font-black text-foreground">Studios (comma-separated)</Label>
<Input <Input
id="studios" id="studios"
value={newMedia.studios} value={newMedia.studios}
onChange={e => setNewMedia(prev => ({ ...prev, studios: e.target.value }))} onChange={e => setNewMedia(prev => ({ ...prev, studios: e.target.value }))}
placeholder="Studio A, Studio B" placeholder="Studio A, Studio B"
className="bg-zinc-50 border-zinc-100 rounded-xl h-11 focus:ring-[#6d28d9]" className="bg-muted border-border rounded-xl h-11 focus:ring-[#6d28d9]"
/> />
</div> </div>
<Button <Button

View File

@@ -92,7 +92,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{/* Genre Filter */} {/* Genre Filter */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedGenre ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}> <button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedGenre ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground")}>
<Star size={16} /> <Star size={16} />
{selectedGenre || 'Genres'} {selectedGenre || 'Genres'}
</button> </button>
@@ -108,7 +108,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{/* Studio Filter */} {/* Studio Filter */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedStudio ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}> <button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedStudio ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground")}>
Studios Studios
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -124,7 +124,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{activeCategory === 'Games' && ( {activeCategory === 'Games' && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedPlatform ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}> <button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedPlatform ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground")}>
<Monitor size={16} /> <Monitor size={16} />
{selectedPlatform || 'Platforms'} {selectedPlatform || 'Platforms'}
</button> </button>
@@ -142,7 +142,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{activeCategory === 'Games' && ( {activeCategory === 'Games' && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedDeveloper ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}> <button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedDeveloper ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground")}>
<Users size={16} /> <Users size={16} />
{selectedDeveloper || 'Developers'} {selectedDeveloper || 'Developers'}
</button> </button>
@@ -160,7 +160,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{activeCategory === 'Games' && ( {activeCategory === 'Games' && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedCategory ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-zinc-600")}> <button type="button" className={cn("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 font-bold gap-2", selectedCategory ? "text-[#6d28d9] bg-[#6d28d9]/10" : "text-muted-foreground")}>
<FolderTree size={16} /> <FolderTree size={16} />
{selectedCategory || 'Categories'} {selectedCategory || 'Categories'}
</button> </button>
@@ -178,7 +178,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
<Button <Button
variant="link" variant="link"
size="sm" size="sm"
className="text-zinc-400 font-bold" className="text-muted-foreground font-bold"
onClick={() => { onClick={() => {
setSelectedGenre(null); setSelectedGenre(null);
setSelectedStudio(null); setSelectedStudio(null);
@@ -195,7 +195,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button type="button" className="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 text-zinc-600 font-bold gap-2"> <button type="button" className="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 text-muted-foreground font-bold gap-2">
<ArrowUpDown size={16} /> <ArrowUpDown size={16} />
{sortBy === 'default' ? 'Sort' : sortBy === 'title-asc' ? 'Title (A-Z)' : 'Title (Z-A)'} {sortBy === 'default' ? 'Sort' : sortBy === 'title-asc' ? 'Title (A-Z)' : 'Title (Z-A)'}
</button> </button>
@@ -207,13 +207,13 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<div className="flex items-center bg-zinc-100 rounded-md p-1"> <div className="flex items-center bg-muted rounded-md p-1">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn( className={cn(
"h-8 w-8 transition-all", "h-8 w-8 transition-all",
viewMode === 'grid' ? "bg-white shadow-sm text-[#6d28d9]" : "text-zinc-400" viewMode === 'grid' ? "bg-background shadow-sm text-[#6d28d9]" : "text-muted-foreground"
)} )}
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
> >
@@ -224,7 +224,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
size="icon" size="icon"
className={cn( className={cn(
"h-8 w-8 transition-all", "h-8 w-8 transition-all",
viewMode === 'list' ? "bg-white shadow-sm text-[#6d28d9]" : "text-zinc-400" viewMode === 'list' ? "bg-background shadow-sm text-[#6d28d9]" : "text-muted-foreground"
)} )}
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
> >
@@ -236,8 +236,8 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{/* Content */} {/* Content */}
{mediaList.length === 0 ? ( {mediaList.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-zinc-400"> <div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<div className="w-16 h-16 bg-zinc-100 rounded-full flex items-center justify-center mb-4"> <div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<Search size={32} /> <Search size={32} />
</div> </div>
<p className="text-lg font-bold">No results found</p> <p className="text-lg font-bold">No results found</p>
@@ -271,16 +271,16 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
{/* Pagination Controls */} {/* Pagination Controls */}
{mediaList.length > 0 && ( {mediaList.length > 0 && (
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-zinc-100 pt-8"> <div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-border pt-8">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-zinc-500 font-medium">Items per page:</span> <span className="text-sm text-muted-foreground font-medium">Items per page:</span>
<select <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => { onChange={(e) => {
setItemsPerPage(Number(e.target.value)); setItemsPerPage(Number(e.target.value));
setCurrentPage(1); setCurrentPage(1);
}} }}
className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-muted border-none rounded-md px-2 py-1 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{[12, 20, 36, 48, 60].map(size => ( {[12, 20, 36, 48, 60].map(size => (
<option key={size} value={size}>{size}</option> <option key={size} value={size}>{size}</option>
@@ -294,7 +294,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
size="sm" size="sm"
onClick={handlePrevPage} onClick={handlePrevPage}
disabled={currentPage === 1} disabled={currentPage === 1}
className="gap-2 font-bold border-zinc-200" className="gap-2 font-bold border-border"
> >
<ChevronLeft size={16} /> <ChevronLeft size={16} />
Previous Previous
@@ -302,8 +302,8 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-black text-[#6d28d9]">{currentPage}</span> <span className="text-sm font-black text-[#6d28d9]">{currentPage}</span>
<span className="text-sm text-zinc-400 font-medium">of</span> <span className="text-sm text-muted-foreground font-medium">of</span>
<span className="text-sm font-bold text-zinc-700">{totalPages || 1}</span> <span className="text-sm font-bold text-foreground">{totalPages || 1}</span>
</div> </div>
<Button <Button
@@ -311,7 +311,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
size="sm" size="sm"
onClick={handleNextPage} onClick={handleNextPage}
disabled={currentPage === totalPages || totalPages === 0} disabled={currentPage === totalPages || totalPages === 0}
className="gap-2 font-bold border-zinc-200" className="gap-2 font-bold border-border"
> >
Next Next
<ChevronRight size={16} /> <ChevronRight size={16} />

View File

@@ -17,7 +17,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
navigate(`/media/${mediaId}`); navigate(`/media/${mediaId}`);
}; };
return ( return (
<div className="min-h-screen bg-white pb-20"> <div className="min-h-screen bg-background pb-20">
{/* Hero Section */} {/* Hero Section */}
<div className="relative h-[40vh] md:h-[50vh] overflow-hidden bg-zinc-900"> <div className="relative h-[40vh] md:h-[50vh] overflow-hidden bg-zinc-900">
<img <img
@@ -26,14 +26,14 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
className="w-full h-full object-cover opacity-40 blur-xl scale-110" className="w-full h-full object-cover opacity-40 blur-xl scale-110"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-white via-transparent to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent" />
<div className="absolute inset-0 flex items-end px-6 pb-12"> <div className="absolute inset-0 flex items-end px-6 pb-12">
<div className="max-w-[1200px] mx-auto w-full flex flex-col md:flex-row items-center md:items-end gap-8"> <div className="max-w-[1200px] mx-auto w-full flex flex-col md:flex-row items-center md:items-end gap-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="w-48 h-48 md:w-64 md:h-64 rounded-2xl overflow-hidden border-4 border-white shadow-2xl shrink-0" className="w-48 h-48 md:w-64 md:h-64 rounded-2xl overflow-hidden border-4 border-background shadow-2xl shrink-0"
> >
<img <img
src={person.photo} src={person.photo}
@@ -49,7 +49,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
> >
<h1 className="text-4xl md:text-6xl font-black text-zinc-900 mb-4 drop-shadow-sm"> <h1 className="text-4xl md:text-6xl font-black text-foreground mb-4 drop-shadow-sm">
{person.name} {person.name}
</h1> </h1>
<div className="flex flex-wrap justify-center md:justify-start gap-3"> <div className="flex flex-wrap justify-center md:justify-start gap-3">
@@ -78,90 +78,90 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
<div className="max-w-[1200px] mx-auto px-6 mt-12 grid grid-cols-1 lg:grid-cols-3 gap-12"> <div className="max-w-[1200px] mx-auto px-6 mt-12 grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Sidebar Info */} {/* Sidebar Info */}
<div className="space-y-8"> <div className="space-y-8">
<div className="bg-zinc-50 rounded-3xl p-8 space-y-6"> <div className="bg-muted/50 rounded-3xl p-8 space-y-6 border border-border">
<h3 className="text-xl font-black text-zinc-900">Personal Info</h3> <h3 className="text-xl font-black text-foreground">Personal Info</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Calendar size={20} /> <Calendar size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Birth Date</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Birth Date</p>
<p className="font-bold text-zinc-700">{person.birthDate || 'Unknown'}</p> <p className="font-bold text-foreground">{person.birthDate || 'Unknown'}</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<MapPin size={20} /> <MapPin size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Birth Place</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Birth Place</p>
<p className="font-bold text-zinc-700">{person.birthPlace || 'Unknown'}</p> <p className="font-bold text-foreground">{person.birthPlace || 'Unknown'}</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Briefcase size={20} /> <Briefcase size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Known For</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Known For</p>
<p className="font-bold text-zinc-700">{person.role}</p> <p className="font-bold text-foreground">{person.role}</p>
</div> </div>
</div> </div>
{(person.ethnicity || person.adult_specifics?.ethnicity) && ( {(person.ethnicity || person.adult_specifics?.ethnicity) && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<User size={20} /> <User size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Ethnicity</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Ethnicity</p>
<p className="font-bold text-zinc-700">{person.adult_specifics?.ethnicity || person.ethnicity}</p> <p className="font-bold text-foreground">{person.adult_specifics?.ethnicity || person.ethnicity}</p>
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="bg-zinc-50 rounded-3xl p-8 space-y-6"> <div className="bg-muted/50 rounded-3xl p-8 space-y-6 border border-border">
<h3 className="text-xl font-black text-zinc-900">Measurements</h3> <h3 className="text-xl font-black text-foreground">Measurements</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Ruler size={20} /> <Ruler size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Height</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Height</p>
<p className="font-bold text-zinc-700">{person.adult_specifics?.height || person.height} cm</p> <p className="font-bold text-foreground">{person.adult_specifics?.height || person.height} cm</p>
</div> </div>
</div> </div>
{(person.weight || person.adult_specifics?.weight) && ( {(person.weight || person.adult_specifics?.weight) && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Ruler size={20} /> <Ruler size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Weight</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Weight</p>
<p className="font-bold text-zinc-700">{person.adult_specifics?.weight || person.weight} kg</p> <p className="font-bold text-foreground">{person.adult_specifics?.weight || person.weight} kg</p>
</div> </div>
</div> </div>
)} )}
{(person.adult_specifics?.measurements || person.bust_size || person.cup_size || person.waist_size || person.hip_size) && ( {(person.adult_specifics?.measurements || person.bust_size || person.cup_size || person.waist_size || person.hip_size) && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Ruler size={20} /> <Ruler size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Measurements</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Measurements</p>
<p className="font-bold text-zinc-700"> <p className="font-bold text-foreground">
{person.adult_specifics?.measurements || ( {person.adult_specifics?.measurements || (
<> <>
{person.bust_size && `${person.bust_size}`} {person.bust_size && `${person.bust_size}`}
@@ -179,48 +179,48 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
{(person.hair_color || person.adult_specifics?.hair_color) && ( {(person.hair_color || person.adult_specifics?.hair_color) && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Palette size={20} /> <Palette size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Hair Color</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Hair Color</p>
<p className="font-bold text-zinc-700">{person.adult_specifics?.hair_color || person.hair_color}</p> <p className="font-bold text-foreground">{person.adult_specifics?.hair_color || person.hair_color}</p>
</div> </div>
</div> </div>
)} )}
{(person.eye_color || person.adult_specifics?.eye_color) && ( {(person.eye_color || person.adult_specifics?.eye_color) && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Eye size={20} /> <Eye size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Eye Color</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Eye Color</p>
<p className="font-bold text-zinc-700">{person.adult_specifics?.eye_color || person.eye_color}</p> <p className="font-bold text-foreground">{person.adult_specifics?.eye_color || person.eye_color}</p>
</div> </div>
</div> </div>
)} )}
{person.adult_specifics?.tattoos && ( {person.adult_specifics?.tattoos && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Palette size={20} /> <Palette size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Tattoos</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Tattoos</p>
<p className="font-bold text-zinc-700">{person.adult_specifics.tattoos}</p> <p className="font-bold text-foreground">{person.adult_specifics.tattoos}</p>
</div> </div>
</div> </div>
)} )}
{person.adult_specifics?.piercings && ( {person.adult_specifics?.piercings && (
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-[#6d28d9] shadow-sm"> <div className="w-10 h-10 rounded-xl bg-background flex items-center justify-center text-[#6d28d9] shadow-sm border border-border">
<Palette size={20} /> <Palette size={20} />
</div> </div>
<div> <div>
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Piercings</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">Piercings</p>
<p className="font-bold text-zinc-700">{person.adult_specifics.piercings}</p> <p className="font-bold text-foreground">{person.adult_specifics.piercings}</p>
</div> </div>
</div> </div>
)} )}
@@ -232,10 +232,10 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
<div className="lg:col-span-2 space-y-12"> <div className="lg:col-span-2 space-y-12">
{person.bio && ( {person.bio && (
<section> <section>
<h2 className="text-2xl font-black text-zinc-900 mb-6 flex items-center gap-3"> <h2 className="text-2xl font-black text-foreground mb-6 flex items-center gap-3">
Biography Biography
</h2> </h2>
<p className="text-zinc-600 leading-relaxed text-lg"> <p className="text-foreground leading-relaxed text-lg">
{person.bio} {person.bio}
</p> </p>
</section> </section>
@@ -243,7 +243,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
{person.filmography && person.filmography.length > 0 && ( {person.filmography && person.filmography.length > 0 && (
<section> <section>
<h2 className="text-2xl font-black text-zinc-900 mb-6 flex items-center gap-3"> <h2 className="text-2xl font-black text-foreground mb-6 flex items-center gap-3">
<User className="text-[#6d28d9]" /> <User className="text-[#6d28d9]" />
Characters Characters
</h2> </h2>
@@ -251,9 +251,9 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
{person.filmography.map(item => ( {person.filmography.map(item => (
<div <div
key={`${item.id}-char`} key={`${item.id}-char`}
className="flex items-center gap-4 p-4 rounded-2xl bg-zinc-50 border border-zinc-100" className="flex items-center gap-4 p-4 rounded-2xl bg-muted/50 border border-border"
> >
<div className="w-20 h-20 rounded-xl overflow-hidden shrink-0 shadow-sm border-2 border-white"> <div className="w-20 h-20 rounded-xl overflow-hidden shrink-0 shadow-sm border-2 border-background">
<img <img
src={item.poster || person.photo} src={item.poster || person.photo}
alt={item.title} alt={item.title}
@@ -262,8 +262,8 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
/> />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest mb-1">Character</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest mb-1">Character</p>
<h4 className="font-black text-zinc-900 truncate">{item.characterName || item.role}</h4> <h4 className="font-black text-foreground truncate">{item.characterName || item.role}</h4>
<button <button
onClick={() => handleMediaClick(item.id.toString())} onClick={() => handleMediaClick(item.id.toString())}
className="text-xs font-bold text-[#6d28d9] hover:underline mt-1 text-left" className="text-xs font-bold text-[#6d28d9] hover:underline mt-1 text-left"
@@ -279,7 +279,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
{person.filmography && person.filmography.length > 0 && ( {person.filmography && person.filmography.length > 0 && (
<section> <section>
<h2 className="text-2xl font-black text-zinc-900 mb-6 flex items-center gap-3"> <h2 className="text-2xl font-black text-foreground mb-6 flex items-center gap-3">
<Film className="text-[#6d28d9]" /> <Film className="text-[#6d28d9]" />
Filmography Filmography
</h2> </h2>
@@ -288,7 +288,7 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
<div <div
key={item.id} key={item.id}
onClick={() => handleMediaClick(item.id.toString())} onClick={() => handleMediaClick(item.id.toString())}
className="group flex items-center gap-4 p-4 rounded-2xl bg-white border border-zinc-100 hover:border-[#6d28d9]/30 hover:shadow-lg transition-all cursor-pointer" className="group flex items-center gap-4 p-4 rounded-2xl bg-card border border-border hover:border-[#6d28d9]/30 hover:shadow-lg transition-all cursor-pointer"
> >
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 shadow-sm"> <div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 shadow-sm">
<img <img
@@ -299,14 +299,14 @@ export default function CastDetailView({ person, relatedMedia }: CastDetailViewP
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h4 className="font-black text-zinc-900 truncate group-hover:text-[#6d28d9] transition-colors"> <h4 className="font-black text-foreground truncate group-hover:text-[#6d28d9] transition-colors">
{item.title} {item.title}
</h4> </h4>
<p className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-1"> <p className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-1">
{item.year || 'Unknown'} {item.year || 'Unknown'}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] font-bold py-0 h-5 border-zinc-200"> <Badge variant="outline" className="text-[10px] font-bold py-0 h-5 border-border">
{item.role} {item.role}
</Badge> </Badge>
</div> </div>

View File

@@ -177,24 +177,24 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<div className="pt-24 pb-12 px-6 max-w-[1200px] mx-auto"> <div className="pt-24 pb-12 px-6 max-w-[1200px] mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-12"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-12">
<div> <div>
<h1 className="text-4xl font-black text-zinc-900 mb-2">Cast & Staff</h1> <h1 className="text-4xl font-black text-foreground mb-2">Cast & Staff</h1>
<p className="text-zinc-500 font-medium">Discover the people behind your favorite media</p> <p className="text-muted-foreground font-medium">Discover the people behind your favorite media</p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={18} />
<Input <Input
placeholder="Search cast..." placeholder="Search cast..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 w-full md:w-[300px] bg-zinc-100 border-none rounded-full h-11" className="pl-10 w-full md:w-[300px] bg-muted border-none rounded-full h-11"
/> />
</div> </div>
<Button <Button
variant={showFilters ? 'default' : 'outline'} variant={showFilters ? 'default' : 'outline'}
size="icon" size="icon"
className={`rounded-full h-11 w-11 ${showFilters ? 'bg-[#6d28d9] text-white border-[#6d28d9]' : 'border-zinc-200'}`} className={`rounded-full h-11 w-11 ${showFilters ? 'bg-[#6d28d9] text-white border-[#6d28d9]' : 'border-border'}`}
onClick={() => setShowFilters(!showFilters)} onClick={() => setShowFilters(!showFilters)}
> >
<Filter size={20} /> <Filter size={20} />
@@ -202,7 +202,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="rounded-full h-11 w-11 border-zinc-200" className="rounded-full h-11 w-11 border-border"
onClick={() => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')} onClick={() => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')}
> >
<ArrowUpDown size={20} /> <ArrowUpDown size={20} />
@@ -211,7 +211,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="rounded-full h-11 w-11 text-zinc-400 hover:text-zinc-900" className="rounded-full h-11 w-11 text-muted-foreground hover:text-foreground"
onClick={handleResetFilters} onClick={handleResetFilters}
title="Reset filters" title="Reset filters"
> >
@@ -226,15 +226,15 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }} animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }} exit={{ opacity: 0, height: 0 }}
className="bg-zinc-50 rounded-2xl p-6 mb-6" className="bg-muted/50 rounded-2xl p-6 mb-6 border border-border"
> >
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<label className="text-sm font-bold text-zinc-700 mb-2 block">Sort By</label> <label className="text-sm font-bold text-foreground mb-2 block">Sort By</label>
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)} onChange={(e) => setSortBy(e.target.value as any)}
className="w-full bg-white border-zinc-200 rounded-lg px-3 py-2 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="w-full bg-background border-border rounded-lg px-3 py-2 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
<option value="name">Name</option> <option value="name">Name</option>
<option value="role">Role</option> <option value="role">Role</option>
@@ -243,11 +243,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
</select> </select>
</div> </div>
<div> <div>
<label className="text-sm font-bold text-zinc-700 mb-2 block">Occupation</label> <label className="text-sm font-bold text-foreground mb-2 block">Occupation</label>
<select <select
value={filterOccupation} value={filterOccupation}
onChange={(e) => setFilterOccupation(e.target.value)} onChange={(e) => setFilterOccupation(e.target.value)}
className="w-full bg-white border-zinc-200 rounded-lg px-3 py-2 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="w-full bg-background border-border rounded-lg px-3 py-2 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
<option value="">All Occupations</option> <option value="">All Occupations</option>
{uniqueOccupations.map(occ => ( {uniqueOccupations.map(occ => (
@@ -256,11 +256,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
</select> </select>
</div> </div>
<div> <div>
<label className="text-sm font-bold text-zinc-700 mb-2 block">Media Type</label> <label className="text-sm font-bold text-foreground mb-2 block">Media Type</label>
<select <select
value={filterMediaType} value={filterMediaType}
onChange={(e) => setFilterMediaType(e.target.value)} onChange={(e) => setFilterMediaType(e.target.value)}
className="w-full bg-white border-zinc-200 rounded-lg px-3 py-2 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="w-full bg-background border-border rounded-lg px-3 py-2 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
<option value="">All Media Types</option> <option value="">All Media Types</option>
{uniqueMediaTypes.map(type => ( {uniqueMediaTypes.map(type => (
@@ -273,7 +273,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{searchQuery && ( {searchQuery && (
<Badge variant="secondary" className="gap-1"> <Badge variant="secondary" className="gap-1">
Search: {searchQuery} Search: {searchQuery}
<button onClick={() => setSearchQuery('')} className="hover:text-zinc-900"> <button onClick={() => setSearchQuery('')} className="hover:text-foreground">
<X size={12} /> <X size={12} />
</button> </button>
</Badge> </Badge>
@@ -281,7 +281,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{filterOccupation && ( {filterOccupation && (
<Badge variant="secondary" className="gap-1"> <Badge variant="secondary" className="gap-1">
Occupation: {filterOccupation} Occupation: {filterOccupation}
<button onClick={() => setFilterOccupation('')} className="hover:text-zinc-900"> <button onClick={() => setFilterOccupation('')} className="hover:text-foreground">
<X size={12} /> <X size={12} />
</button> </button>
</Badge> </Badge>
@@ -289,7 +289,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{filterMediaType && ( {filterMediaType && (
<Badge variant="secondary" className="gap-1"> <Badge variant="secondary" className="gap-1">
Media Type: {filterMediaType} Media Type: {filterMediaType}
<button onClick={() => setFilterMediaType('')} className="hover:text-zinc-900"> <button onClick={() => setFilterMediaType('')} className="hover:text-foreground">
<X size={12} /> <X size={12} />
</button> </button>
</Badge> </Badge>
@@ -297,7 +297,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{(sortBy !== 'name' || sortOrder !== 'asc') && ( {(sortBy !== 'name' || sortOrder !== 'asc') && (
<Badge variant="secondary" className="gap-1"> <Badge variant="secondary" className="gap-1">
Sort: {sortBy} ({sortOrder}) Sort: {sortBy} ({sortOrder})
<button onClick={() => { setSortBy('name'); setSortOrder('asc'); }} className="hover:text-zinc-900"> <button onClick={() => { setSortBy('name'); setSortOrder('asc'); }} className="hover:text-foreground">
<X size={12} /> <X size={12} />
</button> </button>
</Badge> </Badge>
@@ -307,12 +307,12 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
)} )}
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center py-20 text-zinc-400"> <div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#6d28d9] mb-4" /> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#6d28d9] mb-4" />
<p className="text-lg font-bold">Loading cast...</p> <p className="text-lg font-bold">Loading cast...</p>
</div> </div>
) : filteredStaff.length === 0 ? ( ) : filteredStaff.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-zinc-400"> <div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<User size={48} className="mb-4 opacity-20" /> <User size={48} className="mb-4 opacity-20" />
<p className="text-lg font-bold">No cast members found</p> <p className="text-lg font-bold">No cast members found</p>
</div> </div>
@@ -326,11 +326,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }} exit={{ opacity: 0, scale: 0.9 }}
className="group bg-white rounded-2xl p-4 shadow-sm border border-zinc-100 hover:shadow-xl hover:border-[#6d28d9]/20 transition-all duration-300 cursor-pointer" className="group bg-card rounded-2xl p-4 shadow-sm border border-border hover:shadow-xl hover:border-[#6d28d9]/20 transition-all duration-300 cursor-pointer"
onClick={() => onPersonClick(person)} onClick={() => onPersonClick(person)}
> >
<div className="flex items-center gap-4 mb-4"> <div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 rounded-full overflow-hidden border-2 border-zinc-100 group-hover:border-[#6d28d9] transition-colors"> <div className="w-16 h-16 rounded-full overflow-hidden border-2 border-border group-hover:border-[#6d28d9] transition-colors">
<img <img
src={person.photo} src={person.photo}
alt={person.name} alt={person.name}
@@ -339,18 +339,18 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h3 className="font-black text-zinc-900 truncate group-hover:text-[#6d28d9] transition-colors"> <h3 className="font-black text-foreground truncate group-hover:text-[#6d28d9] transition-colors">
{person.name} {person.name}
</h3> </h3>
<p className="text-xs font-bold text-zinc-400 uppercase tracking-wider"> <p className="text-xs font-bold text-muted-foreground uppercase tracking-wider">
{person.role} {person.role}
</p> </p>
</div> </div>
</div> </div>
{person.filmography && person.filmography.length > 0 && ( {person.filmography && person.filmography.length > 0 && (
<div className="bg-zinc-50 rounded-xl p-3 flex items-center gap-3"> <div className="bg-muted/50 rounded-xl p-3 flex items-center gap-3">
<div className="w-10 h-12 rounded-lg overflow-hidden shrink-0 bg-white"> <div className="w-10 h-12 rounded-lg overflow-hidden shrink-0 bg-background">
<img <img
src={person.filmography[0].poster || person.photo} src={person.filmography[0].poster || person.photo}
alt={person.filmography[0].title} alt={person.filmography[0].title}
@@ -359,8 +359,8 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
/> />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest leading-none mb-1">Latest Role</p> <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest leading-none mb-1">Latest Role</p>
<p className="text-xs font-bold text-zinc-700 truncate">{person.filmography[0].title}</p> <p className="text-xs font-bold text-foreground truncate">{person.filmography[0].title}</p>
<p className="text-[10px] text-[#6d28d9] font-bold truncate mt-1">{person.filmography[0].role}</p> <p className="text-[10px] text-[#6d28d9] font-bold truncate mt-1">{person.filmography[0].role}</p>
</div> </div>
</div> </div>
@@ -373,15 +373,15 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{/* Pagination Controls */} {/* Pagination Controls */}
{filteredStaff.length > 0 && ( {filteredStaff.length > 0 && (
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-zinc-100 pt-8"> <div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-border pt-8">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-zinc-500 font-medium">Items per page:</span> <span className="text-sm text-muted-foreground font-medium">Items per page:</span>
<select <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => { onChange={(e) => {
setItemsPerPage(Number(e.target.value)); setItemsPerPage(Number(e.target.value));
}} }}
className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-muted border-none rounded-md px-2 py-1 text-sm font-bold text-foreground focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{[12, 20, 36, 48, 60].map(size => ( {[12, 20, 36, 48, 60].map(size => (
<option key={size} value={size}>{size}</option> <option key={size} value={size}>{size}</option>
@@ -395,7 +395,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
size="sm" size="sm"
onClick={handlePrevPage} onClick={handlePrevPage}
disabled={currentPage === 1} disabled={currentPage === 1}
className="gap-2 font-bold border-zinc-200" className="gap-2 font-bold border-border"
> >
<ChevronLeft size={16} /> <ChevronLeft size={16} />
Previous Previous
@@ -403,8 +403,8 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-black text-[#6d28d9]">{currentPage}</span> <span className="text-sm font-black text-[#6d28d9]">{currentPage}</span>
<span className="text-sm text-zinc-400 font-medium">of</span> <span className="text-sm text-muted-foreground font-medium">of</span>
<span className="text-sm font-bold text-zinc-700">{totalPages || 1}</span> <span className="text-sm font-bold text-foreground">{totalPages || 1}</span>
</div> </div>
<Button <Button
@@ -412,7 +412,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
size="sm" size="sm"
onClick={handleNextPage} onClick={handleNextPage}
disabled={currentPage === totalPages || totalPages === 0} disabled={currentPage === totalPages || totalPages === 0}
className="gap-2 font-bold border-zinc-200" className="gap-2 font-bold border-border"
> >
Next Next
<ChevronRight size={16} /> <ChevronRight size={16} />

View File

@@ -24,7 +24,7 @@ interface DetailViewProps {
export default function DetailView({ media, onPersonClick }: DetailViewProps) { export default function DetailView({ media, onPersonClick }: DetailViewProps) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="min-h-screen bg-zinc-50"> <div className="min-h-screen bg-background">
{/* Banner */} {/* Banner */}
<div className="relative h-[400px] w-full overflow-hidden"> <div className="relative h-[400px] w-full overflow-hidden">
<img <img
@@ -33,7 +33,7 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
className="w-full h-full object-cover" className="w-full h-full object-cover"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-zinc-50 via-zinc-50/40 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent" />
<button <button
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
@@ -50,7 +50,7 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
<div className="w-full md:w-[300px] shrink-0"> <div className="w-full md:w-[300px] shrink-0">
<motion.div <motion.div
layoutId={`media-${media.id}`} layoutId={`media-${media.id}`}
className={`rounded-xl overflow-hidden shadow-2xl bg-zinc-800 ${ className={`rounded-xl overflow-hidden shadow-2xl bg-card ${
media.aspectRatio === '16/9' ? 'aspect-video' : media.aspectRatio === '16/9' ? 'aspect-video' :
media.aspectRatio === '1/1' ? 'aspect-square' : media.aspectRatio === '1/1' ? 'aspect-square' :
'aspect-[2/3]' 'aspect-[2/3]'
@@ -69,22 +69,22 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
<div className="flex-1 pt-32 md:pt-40"> <div className="flex-1 pt-32 md:pt-40">
<div className="flex flex-wrap items-end justify-between gap-4 mb-6"> <div className="flex flex-wrap items-end justify-between gap-4 mb-6">
<div> <div>
<h1 className="text-4xl font-black text-zinc-900 mb-2"> <h1 className="text-4xl font-black text-foreground mb-2">
{media.title} <span className="text-zinc-400 font-medium">({media.year})</span> {media.title} <span className="text-muted-foreground font-medium">({media.year})</span>
</h1> </h1>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button size="icon" className="rounded-full bg-[#6d28d9] hover:bg-[#5b21b6]"> <Button size="icon" className="rounded-full bg-[#6d28d9] hover:bg-[#5b21b6]">
<Play size={20} fill="currentColor" /> <Play size={20} fill="currentColor" />
</Button> </Button>
<Button size="icon" variant="outline" className="rounded-full border-zinc-300"> <Button size="icon" variant="outline" className="rounded-full border-border">
<Bookmark size={20} /> <Bookmark size={20} />
</Button> </Button>
<Button size="icon" variant="outline" className="rounded-full border-zinc-300"> <Button size="icon" variant="outline" className="rounded-full border-border">
<MoreHorizontal size={20} /> <MoreHorizontal size={20} />
</Button> </Button>
</div> </div>
<div className="flex items-center gap-1 text-zinc-600 font-bold"> <div className="flex items-center gap-1 text-foreground font-bold">
<Star size={18} className="text-yellow-500" fill="currentColor" /> <Star size={18} className="text-yellow-500" fill="currentColor" />
{media.rating} / 10 {media.rating} / 10
</div> </div>
@@ -95,7 +95,7 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
<h3 className="text-xs font-black text-[#6d28d9] uppercase tracking-wider mb-2">Genres</h3> <h3 className="text-xs font-black text-[#6d28d9] uppercase tracking-wider mb-2">Genres</h3>
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
{media.genres?.map(genre => ( {media.genres?.map(genre => (
<span key={genre} className="text-sm font-bold text-zinc-600 hover:text-[#6d28d9] cursor-pointer transition-colors"> <span key={genre} className="text-sm font-bold text-foreground hover:text-[#6d28d9] cursor-pointer transition-colors">
{genre} {genre}
</span> </span>
))} ))}
@@ -103,7 +103,7 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
</div> </div>
</div> </div>
<p className="text-zinc-600 leading-relaxed mb-8 max-w-3xl"> <p className="text-foreground leading-relaxed mb-8 max-w-3xl">
{media.description} {media.description}
</p> </p>
@@ -118,16 +118,16 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
<div className="space-y-4"> <div className="space-y-4">
{media.studios && media.studios.length > 0 && ( {media.studios && media.studios.length > 0 && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Studios:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Studios:</span>
{media.studios.join(', ')} {media.studios.join(', ')}
</p> </p>
)} )}
{media.developers && media.developers.length > 0 && ( {media.developers && media.developers.length > 0 && (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Developers:</span> <span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Developers:</span>
{media.developers.map(dev => ( {media.developers.map(dev => (
<Badge key={dev} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]"> <Badge key={dev} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-3 py-1 font-bold text-[10px]">
{dev} {dev}
</Badge> </Badge>
))} ))}
@@ -135,9 +135,9 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
)} )}
{media.platforms && media.platforms.length > 0 && ( {media.platforms && media.platforms.length > 0 && (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Platforms:</span> <span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Platforms:</span>
{media.platforms.map(platform => ( {media.platforms.map(platform => (
<Badge key={platform} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]"> <Badge key={platform} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-3 py-1 font-bold text-[10px]">
{platform} {platform}
</Badge> </Badge>
))} ))}
@@ -145,46 +145,46 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
)} )}
{media.categories && media.categories.length > 0 && ( {media.categories && media.categories.length > 0 && (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Categories:</span> <span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Categories:</span>
{media.categories.map(category => ( {media.categories.map(category => (
<Badge key={category} variant="secondary" className="bg-zinc-100 text-zinc-700 hover:bg-zinc-200 border-none px-3 py-1 font-bold text-[10px]"> <Badge key={category} variant="secondary" className="bg-muted text-foreground hover:bg-muted/80 border-none px-3 py-1 font-bold text-[10px]">
{category} {category}
</Badge> </Badge>
))} ))}
</div> </div>
)} )}
{media.completionStatus && ( {media.completionStatus && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Completion:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Completion:</span>
{media.completionStatus} {media.completionStatus}
</p> </p>
)} )}
{media.source && ( {media.source && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Source:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Source:</span>
{media.source} {media.source}
</p> </p>
)} )}
{media.playCount !== undefined && media.playCount !== null && ( {media.playCount !== undefined && media.playCount !== null && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Play Count:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Play Count:</span>
{media.playCount} {media.playCount}
</p> </p>
)} )}
{media.playtime !== undefined && media.playtime !== null && media.playtime > 0 && ( {media.playtime !== undefined && media.playtime !== null && media.playtime > 0 && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Playtime:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Playtime:</span>
{media.playtime}h {media.playtime}h
</p> </p>
)} )}
{media.lastActivity && ( {media.lastActivity && (
<p className="text-xs font-bold text-zinc-500"> <p className="text-xs font-bold text-muted-foreground">
<span className="text-zinc-400 uppercase tracking-widest mr-2">Last Activity:</span> <span className="text-muted-foreground/70 uppercase tracking-widest mr-2">Last Activity:</span>
{media.lastActivity} {media.lastActivity}
</p> </p>
)} )}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-xs font-bold text-zinc-400 uppercase tracking-widest">Links:</span> <span className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Links:</span>
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">Tvdb</Button> <Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">Tvdb</Button>
<Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">AniDb</Button> <Button variant="link" className="p-0 h-auto text-[#6d28d9] font-bold text-xs">AniDb</Button>
</div> </div>
@@ -196,12 +196,12 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
{media.staff && media.staff.length > 0 && ( {media.staff && media.staff.length > 0 && (
<section className="mt-20"> <section className="mt-20">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-black text-zinc-900">Cast & Crew</h2> <h2 className="text-2xl font-black text-foreground">Cast & Crew</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" size="icon" className="rounded-full border-zinc-200"> <Button variant="outline" size="icon" className="rounded-full border-border">
<ChevronLeft size={18} /> <ChevronLeft size={18} />
</Button> </Button>
<Button variant="outline" size="icon" className="rounded-full border-zinc-200"> <Button variant="outline" size="icon" className="rounded-full border-border">
<ChevronRight size={18} /> <ChevronRight size={18} />
</Button> </Button>
</div> </div>
@@ -210,17 +210,17 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
{media.staff.map(person => ( {media.staff.map(person => (
<div <div
key={person.id} key={person.id}
className="flex items-center gap-4 bg-white p-3 rounded-xl shadow-sm border border-zinc-100 hover:shadow-md transition-shadow cursor-pointer group" className="flex items-center gap-4 bg-card p-3 rounded-xl shadow-sm border border-border hover:shadow-md transition-shadow cursor-pointer group"
onClick={() => onPersonClick(person)} onClick={() => onPersonClick(person)}
> >
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0"> <div className="w-16 h-20 rounded-lg overflow-hidden shrink-0">
<img src={person.photo} alt={person.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform" referrerPolicy="no-referrer" /> <img src={person.photo} alt={person.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform" referrerPolicy="no-referrer" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-bold text-zinc-900 truncate group-hover:text-[#6d28d9] transition-colors">{person.name}</h4> <h4 className="font-bold text-foreground truncate group-hover:text-[#6d28d9] transition-colors">{person.name}</h4>
<p className="text-xs text-zinc-500 truncate">{person.role}</p> <p className="text-xs text-muted-foreground truncate">{person.role}</p>
</div> </div>
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 bg-zinc-50"> <div className="w-16 h-20 rounded-lg overflow-hidden shrink-0 bg-muted">
<img src={person.characterImage} alt={person.characterName} className="w-full h-full object-contain" referrerPolicy="no-referrer" /> <img src={person.characterImage} alt={person.characterName} className="w-full h-full object-contain" referrerPolicy="no-referrer" />
</div> </div>
</div> </div>
@@ -240,13 +240,13 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
<Input placeholder="Search" className="pl-10 w-[200px] bg-zinc-100 border-none rounded-full h-9 text-sm" /> <Input placeholder="Search" className="pl-10 w-[200px] bg-muted border-none rounded-full h-9 text-sm" />
</div> </div>
<Button variant="ghost" size="icon" className="text-zinc-400"> <Button variant="ghost" size="icon" className="text-muted-foreground">
<MoreHorizontal size={20} /> <MoreHorizontal size={20} />
</Button> </Button>
<Button variant="ghost" size="icon" className="text-zinc-400"> <Button variant="ghost" size="icon" className="text-muted-foreground">
<ListFilter size={20} /> <ListFilter size={20} />
</Button> </Button>
</div> </div>
@@ -262,17 +262,17 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
</div> </div>
<div className="flex-1 py-1"> <div className="flex-1 py-1">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h3 className="font-black text-zinc-900 group-hover:text-[#6d28d9] transition-colors"> <h3 className="font-black text-foreground group-hover:text-[#6d28d9] transition-colors">
S1:E{episode.number} {episode.title} S1:E{episode.number} {episode.title}
</h3> </h3>
<span className="text-xs font-bold text-zinc-400">{episode.date} {episode.duration}</span> <span className="text-xs font-bold text-muted-foreground">{episode.date} {episode.duration}</span>
</div> </div>
<p className="text-sm text-zinc-500 leading-relaxed line-clamp-3"> <p className="text-sm text-muted-foreground leading-relaxed line-clamp-3">
{episode.description} {episode.description}
</p> </p>
</div> </div>
</div> </div>
<Separator className="mt-6 bg-zinc-200" /> <Separator className="mt-6 bg-border" />
</div> </div>
))} ))}
</div> </div>

View File

@@ -164,13 +164,13 @@ export default function ImporterView() {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => navigate('/')} onClick={() => navigate('/')}
className="text-zinc-600 hover:text-[#6d28d9]" className="text-muted-foreground hover:text-[#6d28d9]"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} />
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-black text-zinc-900">Media Importers</h1> <h1 className="text-2xl font-black text-foreground">Media Importers</h1>
<p className="text-sm text-zinc-500 font-medium">Import media from external platforms</p> <p className="text-sm text-muted-foreground font-medium">Import media from external platforms</p>
</div> </div>
</div> </div>
</div> </div>
@@ -179,38 +179,38 @@ export default function ImporterView() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{/* XBVR Importer Card */} {/* XBVR Importer Card */}
{xbvrConfig.url && ( {xbvrConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors"> <div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center"> <div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<Film className="text-purple-600" size={24} /> <Film className="text-purple-600" size={24} />
</div> </div>
<div> <div>
<h3 className="font-bold text-zinc-900">XBVR</h3> <h3 className="font-bold text-foreground">XBVR</h3>
<p className="text-xs text-zinc-500 font-medium">Adult Video Manager</p> <p className="text-xs text-muted-foreground font-medium">Adult Video Manager</p>
</div> </div>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8 border-zinc-200" className="h-8 w-8 border-border"
onClick={() => {}} onClick={() => {}}
> >
<Settings size={16} /> <Settings size={16} />
</Button> </Button>
</div> </div>
<p className="text-sm text-zinc-600 mb-4"> <p className="text-sm text-muted-foreground mb-4">
Import adult videos and actors from your XBVR database. Import adult videos and actors from your XBVR database.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">XBVR URL</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">XBVR URL</label>
<input <input
type="text" type="text"
value={xbvrConfig.url} value={xbvrConfig.url}
onChange={(e) => setXbvrConfig({ ...xbvrConfig, url: e.target.value })} onChange={(e) => setXbvrConfig({ ...xbvrConfig, url: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="http://192.168.1.102:10001" placeholder="http://192.168.1.102:10001"
/> />
</div> </div>
@@ -237,49 +237,49 @@ export default function ImporterView() {
{/* StashAPP Importer Card */} {/* StashAPP Importer Card */}
{stashappConfig.url && ( {stashappConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors"> <div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center"> <div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<Film className="text-blue-600" size={24} /> <Film className="text-blue-600" size={24} />
</div> </div>
<div> <div>
<h3 className="font-bold text-zinc-900">StashAPP</h3> <h3 className="font-bold text-foreground">StashAPP</h3>
<p className="text-xs text-zinc-500 font-medium">Adult Content Manager</p> <p className="text-xs text-muted-foreground font-medium">Adult Content Manager</p>
</div> </div>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8 border-zinc-200" className="h-8 w-8 border-border"
onClick={() => {}} onClick={() => {}}
> >
<Settings size={16} /> <Settings size={16} />
</Button> </Button>
</div> </div>
<p className="text-sm text-zinc-600 mb-4"> <p className="text-sm text-muted-foreground mb-4">
Import adult videos and performers from your StashAPP database. Import adult videos and performers from your StashAPP database.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">StashAPP URL</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">StashAPP URL</label>
<input <input
type="text" type="text"
value={stashappConfig.url} value={stashappConfig.url}
onChange={(e) => setStashappConfig({ ...stashappConfig, url: e.target.value })} onChange={(e) => setStashappConfig({ ...stashappConfig, url: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="http://192.168.1.102:10001" placeholder="http://192.168.1.102:10001"
/> />
</div> </div>
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Key (optional)</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">API Key (optional)</label>
<input <input
type="password" type="password"
value={stashappConfig.apiKey || ''} value={stashappConfig.apiKey || ''}
onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })} onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="Enter API key if required" placeholder="Enter API key if required"
/> />
</div> </div>
@@ -306,38 +306,38 @@ export default function ImporterView() {
{/* StashAPP Actor Updater Card */} {/* StashAPP Actor Updater Card */}
{stashappConfig.url && ( {stashappConfig.url && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors"> <div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center"> <div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<Users className="text-green-600" size={24} /> <Users className="text-green-600" size={24} />
</div> </div>
<div> <div>
<h3 className="font-bold text-zinc-900">StashAPP Actor Updater</h3> <h3 className="font-bold text-foreground">StashAPP Actor Updater</h3>
<p className="text-xs text-zinc-500 font-medium">Update existing actors</p> <p className="text-xs text-muted-foreground font-medium">Update existing actors</p>
</div> </div>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8 border-zinc-200" className="h-8 w-8 border-border"
onClick={() => {}} onClick={() => {}}
> >
<Settings size={16} /> <Settings size={16} />
</Button> </Button>
</div> </div>
<p className="text-sm text-zinc-600 mb-4"> <p className="text-sm text-muted-foreground mb-4">
Update existing actors with fresh data from StashAPP and create missing ones. Update existing actors with fresh data from StashAPP and create missing ones.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Key (optional)</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">API Key (optional)</label>
<input <input
type="password" type="password"
value={stashappConfig.apiKey || ''} value={stashappConfig.apiKey || ''}
onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })} onChange={(e) => setStashappConfig({ ...stashappConfig, apiKey: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="Enter API key if required" placeholder="Enter API key if required"
/> />
</div> </div>
@@ -364,60 +364,60 @@ export default function ImporterView() {
{/* Playnite Importer Card */} {/* Playnite Importer Card */}
{playniteConfig.ip && playniteConfig.apiToken && ( {playniteConfig.ip && playniteConfig.apiToken && (
<div className="bg-white border border-zinc-200 rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors"> <div className="bg-card border border-border rounded-xl p-6 hover:border-[#6d28d9]/50 transition-colors">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center"> <div className="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
<Film className="text-orange-600" size={24} /> <Film className="text-orange-600" size={24} />
</div> </div>
<div> <div>
<h3 className="font-bold text-zinc-900">Playnite</h3> <h3 className="font-bold text-foreground">Playnite</h3>
<p className="text-xs text-zinc-500 font-medium">Game Library Manager</p> <p className="text-xs text-muted-foreground font-medium">Game Library Manager</p>
</div> </div>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8 border-zinc-200" className="h-8 w-8 border-border"
onClick={() => {}} onClick={() => {}}
> >
<Settings size={16} /> <Settings size={16} />
</Button> </Button>
</div> </div>
<p className="text-sm text-zinc-600 mb-4"> <p className="text-sm text-muted-foreground mb-4">
Import games from your Playnite library via Bridge API. Import games from your Playnite library via Bridge API.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">IP Address</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">IP Address</label>
<input <input
type="text" type="text"
value={playniteConfig.ip} value={playniteConfig.ip}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, ip: e.target.value })} onChange={(e) => setPlayniteConfig({ ...playniteConfig, ip: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="localhost" placeholder="localhost"
/> />
</div> </div>
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">Port</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">Port</label>
<input <input
type="number" type="number"
value={playniteConfig.port || 19821} value={playniteConfig.port || 19821}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, port: parseInt(e.target.value) || 19821 })} onChange={(e) => setPlayniteConfig({ ...playniteConfig, port: parseInt(e.target.value) || 19821 })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="19821" placeholder="19821"
/> />
</div> </div>
<div> <div>
<label className="text-xs font-bold text-zinc-500 mb-1 block">API Token</label> <label className="text-xs font-bold text-muted-foreground mb-1 block">API Token</label>
<input <input
type="password" type="password"
value={playniteConfig.apiToken} value={playniteConfig.apiToken}
onChange={(e) => setPlayniteConfig({ ...playniteConfig, apiToken: e.target.value })} onChange={(e) => setPlayniteConfig({ ...playniteConfig, apiToken: e.target.value })}
disabled={progress.stage !== 'idle'} disabled={progress.stage !== 'idle'}
className="w-full px-3 py-2 text-sm border border-zinc-200 rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-zinc-100 disabled:cursor-not-allowed" className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed"
placeholder="pb_your_token_here" placeholder="pb_your_token_here"
/> />
</div> </div>
@@ -445,7 +445,7 @@ export default function ImporterView() {
{/* Progress Section */} {/* Progress Section */}
{progress.stage !== 'idle' && ( {progress.stage !== 'idle' && (
<div className="bg-white border border-zinc-200 rounded-xl p-6"> <div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{progress.stage === 'complete' ? ( {progress.stage === 'complete' ? (
@@ -458,12 +458,12 @@ export default function ImporterView() {
</div> </div>
) : ( ) : (
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center"> <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<Loader2 className="text-purple-600 animate-spin" size={20} /> <Loader2 className="text-muted-foreground animate-spin" size={20} />
</div> </div>
)} )}
<div> <div>
<h3 className="font-bold text-zinc-900">{progress.message}</h3> <h3 className="font-bold text-foreground">{progress.message}</h3>
<p className="text-xs text-zinc-500 font-medium"> <p className="text-xs text-muted-foreground font-medium">
{progress.stage === 'fetching' && 'Connecting to external service...'} {progress.stage === 'fetching' && 'Connecting to external service...'}
{progress.stage === 'importing' && `Processing items... ${getProgressPercentage()}%`} {progress.stage === 'importing' && `Processing items... ${getProgressPercentage()}%`}
{progress.stage === 'complete' && 'Import finished'} {progress.stage === 'complete' && 'Import finished'}
@@ -476,7 +476,7 @@ export default function ImporterView() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={resetImport} onClick={resetImport}
className="gap-2 font-bold border-zinc-200" className="gap-2 font-bold border-border"
> >
<RefreshCw size={16} /> <RefreshCw size={16} />
Reset Reset
@@ -487,7 +487,7 @@ export default function ImporterView() {
{/* Progress Bar */} {/* Progress Bar */}
{progress.stage === 'fetching' || progress.stage === 'importing' ? ( {progress.stage === 'fetching' || progress.stage === 'importing' ? (
<div className="mb-6"> <div className="mb-6">
<div className="h-2 bg-zinc-100 rounded-full overflow-hidden"> <div className="h-2 bg-muted rounded-full overflow-hidden">
<div <div
className={cn( className={cn(
"h-full transition-all duration-300 ease-out", "h-full transition-all duration-300 ease-out",
@@ -496,7 +496,7 @@ export default function ImporterView() {
style={{ width: `${getProgressPercentage()}%` }} style={{ width: `${getProgressPercentage()}%` }}
/> />
</div> </div>
<div className="flex justify-between mt-2 text-xs text-zinc-500 font-medium"> <div className="flex justify-between mt-2 text-xs text-muted-foreground font-medium">
<span>{progress.current} / {progress.total} items</span> <span>{progress.current} / {progress.total} items</span>
<span>{getProgressPercentage()}%</span> <span>{getProgressPercentage()}%</span>
</div> </div>
@@ -505,26 +505,26 @@ export default function ImporterView() {
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-zinc-50 rounded-lg p-4"> <div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Film size={16} className="text-zinc-400" /> <Film size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-zinc-500">{(progress as any).gamesImported !== undefined ? 'Games' : 'Videos'}</span> <span className="text-xs font-bold text-muted-foreground">{(progress as any).gamesImported !== undefined ? 'Games' : 'Videos'}</span>
</div> </div>
<p className="text-2xl font-black text-zinc-900">{(progress as any).gamesImported !== undefined ? (progress as any).gamesImported : progress.videosImported}</p> <p className="text-2xl font-black text-foreground">{(progress as any).gamesImported !== undefined ? (progress as any).gamesImported : progress.videosImported}</p>
</div> </div>
<div className="bg-zinc-50 rounded-lg p-4"> <div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Users size={16} className="text-zinc-400" /> <Users size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-zinc-500">Actors</span> <span className="text-xs font-bold text-muted-foreground">Actors</span>
</div> </div>
<p className="text-2xl font-black text-zinc-900">{progress.actorsImported}</p> <p className="text-2xl font-black text-foreground">{progress.actorsImported}</p>
</div> </div>
<div className="bg-zinc-50 rounded-lg p-4"> <div className="bg-muted rounded-lg p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<AlertCircle size={16} className="text-zinc-400" /> <AlertCircle size={16} className="text-muted-foreground" />
<span className="text-xs font-bold text-zinc-500">Errors</span> <span className="text-xs font-bold text-muted-foreground">Errors</span>
</div> </div>
<p className="text-2xl font-black text-zinc-900">{progress.errors.length}</p> <p className="text-2xl font-black text-foreground">{progress.errors.length}</p>
</div> </div>
</div> </div>

View File

@@ -52,7 +52,7 @@ export default function MediaCard({ media, onClick }: MediaCardProps) {
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<div className={cn( <div className={cn(
"relative rounded-lg overflow-hidden shadow-lg bg-zinc-800 transition-all duration-300", "relative rounded-lg overflow-hidden shadow-lg bg-card transition-all duration-300",
aspectRatioClass aspectRatioClass
)}> )}>
<img <img
@@ -70,10 +70,10 @@ export default function MediaCard({ media, onClick }: MediaCardProps) {
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" /> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" />
</div> </div>
<div className="mt-3 space-y-1"> <div className="mt-3 space-y-1">
<h3 className="text-sm font-bold text-zinc-900 line-clamp-1 group-hover:text-[#6d28d9] transition-colors"> <h3 className="text-sm font-bold text-foreground line-clamp-1 group-hover:text-[#6d28d9] transition-colors">
{media.title} {media.title}
</h3> </h3>
<p className="text-xs font-medium text-zinc-500"> <p className="text-xs font-medium text-muted-foreground">
{media.year} {media.year}
</p> </p>
</div> </div>

View File

@@ -44,11 +44,11 @@ export default function MediaListItem({ media, onClick }: MediaListItemProps) {
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -10 }}
className="group flex items-center gap-6 p-4 rounded-xl hover:bg-zinc-50 transition-colors cursor-pointer border border-transparent hover:border-zinc-200" className="group flex items-center gap-6 p-4 rounded-xl hover:bg-muted/50 transition-colors cursor-pointer border border-transparent hover:border-border"
onClick={() => onClick(media)} onClick={() => onClick(media)}
> >
<div className={cn( <div className={cn(
"relative rounded-lg overflow-hidden shrink-0 shadow-md bg-zinc-800 transition-all duration-300", "relative rounded-lg overflow-hidden shrink-0 shadow-md bg-card transition-all duration-300",
aspectRatioClass aspectRatioClass
)}> )}>
<img <img
@@ -67,32 +67,32 @@ export default function MediaListItem({ media, onClick }: MediaListItemProps) {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<h3 className="text-lg font-black text-zinc-900 truncate group-hover:text-[#6d28d9] transition-colors"> <h3 className="text-lg font-black text-foreground truncate group-hover:text-[#6d28d9] transition-colors">
{media.title} {media.title}
</h3> </h3>
<span className="text-sm font-bold text-zinc-400">({media.year})</span> <span className="text-sm font-bold text-muted-foreground">({media.year})</span>
</div> </div>
<div className="flex items-center gap-4 mb-3"> <div className="flex items-center gap-4 mb-3">
<div className="flex items-center gap-1 text-xs font-bold text-zinc-500"> <div className="flex items-center gap-1 text-xs font-bold text-muted-foreground">
<Star size={14} className="text-yellow-500" fill="currentColor" /> <Star size={14} className="text-yellow-500" fill="currentColor" />
{media.rating || 'N/A'} {media.rating || 'N/A'}
</div> </div>
<div className="text-xs font-bold text-zinc-400 uppercase tracking-wider"> <div className="text-xs font-bold text-muted-foreground uppercase tracking-wider">
{media.genres?.slice(0, 3).join(' • ') || 'Anime'} {media.genres?.slice(0, 3).join(' • ') || 'Anime'}
</div> </div>
</div> </div>
<p className="text-sm text-zinc-500 line-clamp-2 max-w-2xl"> <p className="text-sm text-muted-foreground line-clamp-2 max-w-2xl">
{media.description || "No description available for this title."} {media.description || "No description available for this title."}
</p> </p>
</div> </div>
<div className="hidden md:flex items-center gap-2"> <div className="hidden md:flex items-center gap-2">
<Button size="icon" variant="ghost" className="rounded-full text-zinc-400 hover:text-[#6d28d9] hover:bg-[#6d28d9]/10"> <Button size="icon" variant="ghost" className="rounded-full text-muted-foreground hover:text-[#6d28d9] hover:bg-[#6d28d9]/10">
<Play size={18} fill="currentColor" /> <Play size={18} fill="currentColor" />
</Button> </Button>
<Button size="icon" variant="ghost" className="rounded-full text-zinc-400 hover:text-[#6d28d9] hover:bg-[#6d28d9]/10"> <Button size="icon" variant="ghost" className="rounded-full text-muted-foreground hover:text-[#6d28d9] hover:bg-[#6d28d9]/10">
<Bookmark size={18} /> <Bookmark size={18} />
</Button> </Button>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label';
import { Film, Music, Book, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon, Save, ArrowLeft } from 'lucide-react'; import { Film, Music, Book, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon, Save, ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { fetchSettings, updateSettings } from '@/api'; import { fetchSettings, updateSettings } from '@/api';
import { useTheme } from '@/contexts/ThemeContext';
const CATEGORY_ICONS: Record<MediaCategory, React.ReactNode> = { const CATEGORY_ICONS: Record<MediaCategory, React.ReactNode> = {
Anime: <Tv size={18} />, Anime: <Tv size={18} />,
@@ -31,6 +32,7 @@ interface SettingsViewProps {
} }
export default function SettingsView({ onSettingsSaved }: SettingsViewProps) { export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const { setTheme } = useTheme();
const [settings, setSettings] = useState<UserSettings>({ const [settings, setSettings] = useState<UserSettings>({
enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'], enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'],
itemsPerPage: 20, itemsPerPage: 20,
@@ -69,6 +71,8 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
if (savedSettings) { if (savedSettings) {
setSettings(savedSettings); setSettings(savedSettings);
setSaveStatus('success'); setSaveStatus('success');
// Sync theme with theme context
setTheme(savedSettings.theme);
onSettingsSaved?.(); onSettingsSaved?.();
} else { } else {
setSaveStatus('error'); setSaveStatus('error');
@@ -93,26 +97,26 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-white flex items-center justify-center"> <div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-zinc-400 font-medium">Loading settings...</div> <div className="text-muted-foreground font-medium">Loading settings...</div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-white pt-20"> <div className="min-h-screen bg-background pt-20">
{/* Content */} {/* Content */}
<div className="max-w-[1600px] mx-auto px-6 py-12"> <div className="max-w-[1600px] mx-auto px-6 py-12">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-sm font-bold text-zinc-400 hover:text-[#6d28d9] transition-colors mb-2" className="inline-flex items-center gap-2 text-sm font-bold text-muted-foreground hover:text-[#6d28d9] transition-colors mb-2"
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
Back to home Back to home
</Link> </Link>
<h1 className="text-3xl font-black text-zinc-900">Settings</h1> <h1 className="text-3xl font-black text-foreground">Settings</h1>
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
@@ -144,23 +148,23 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
<div className="grid gap-8"> <div className="grid gap-8">
{/* Library Settings */} {/* Library Settings */}
<section> <section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Library Settings</h2> <h2 className="text-xl font-black text-foreground mb-6">Library Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100"> <div className="bg-muted/50 rounded-2xl p-6 border border-border">
<p className="text-sm font-medium text-zinc-500 mb-4"> <p className="text-sm font-medium text-muted-foreground mb-4">
Toggle which media areas you want to see in your library. Toggle which media areas you want to see in your library.
</p> </p>
<div className="grid gap-4"> <div className="grid gap-4">
{(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => ( {(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => (
<div key={category} className="flex items-center justify-between p-4 rounded-xl bg-white border border-zinc-100 transition-all hover:border-[#6d28d9]/20"> <div key={category} className="flex items-center justify-between p-4 rounded-xl bg-background border border-border transition-all hover:border-[#6d28d9]/20">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-zinc-50 flex items-center justify-center text-[#6d28d9]"> <div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center text-[#6d28d9]">
{CATEGORY_ICONS[category]} {CATEGORY_ICONS[category]}
</div> </div>
<div> <div>
<Label htmlFor={category} className="text-sm font-black text-zinc-900 cursor-pointer"> <Label htmlFor={category} className="text-sm font-black text-foreground cursor-pointer">
{category} {category}
</Label> </Label>
<p className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest"> <p className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'} {settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'}
</p> </p>
</div> </div>
@@ -178,11 +182,11 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
{/* Display Settings */} {/* Display Settings */}
<section> <section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Display Settings</h2> <h2 className="text-xl font-black text-foreground mb-6">Display Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100 space-y-6"> <div className="bg-muted/50 rounded-2xl p-6 border border-border space-y-6">
{/* Items per page */} {/* Items per page */}
<div> <div>
<Label className="text-sm font-black text-zinc-900 mb-2 block">Items per page</Label> <Label className="text-sm font-black text-foreground mb-2 block">Items per page</Label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{ITEMS_PER_PAGE_OPTIONS.map((option) => ( {ITEMS_PER_PAGE_OPTIONS.map((option) => (
<button <button
@@ -191,7 +195,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${ className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${
settings.itemsPerPage === option settings.itemsPerPage === option
? 'bg-[#6d28d9] text-white' ? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200' : 'bg-background text-foreground hover:bg-muted border border-border'
}`} }`}
> >
{option} {option}
@@ -202,14 +206,14 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
{/* Default view */} {/* Default view */}
<div> <div>
<Label className="text-sm font-black text-zinc-900 mb-2 block">Default view</Label> <Label className="text-sm font-black text-foreground mb-2 block">Default view</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))} onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${ className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${
settings.defaultView === 'grid' settings.defaultView === 'grid'
? 'bg-[#6d28d9] text-white' ? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200' : 'bg-background text-foreground hover:bg-muted border border-border'
}`} }`}
> >
<LayoutGrid size={18} /> <LayoutGrid size={18} />
@@ -220,7 +224,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${ className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${
settings.defaultView === 'list' settings.defaultView === 'list'
? 'bg-[#6d28d9] text-white' ? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200' : 'bg-background text-foreground hover:bg-muted border border-border'
}`} }`}
> >
<List size={18} /> <List size={18} />
@@ -231,7 +235,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
{/* Theme */} {/* Theme */}
<div> <div>
<Label className="text-sm font-black text-zinc-900 mb-2 block">Theme</Label> <Label className="text-sm font-black text-foreground mb-2 block">Theme</Label>
<div className="flex gap-2"> <div className="flex gap-2">
{(['light', 'dark', 'system'] as const).map((theme) => ( {(['light', 'dark', 'system'] as const).map((theme) => (
<button <button
@@ -240,7 +244,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${ className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg text-sm font-bold transition-all ${
settings.theme === theme settings.theme === theme
? 'bg-[#6d28d9] text-white' ? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200' : 'bg-background text-foreground hover:bg-muted border border-border'
}`} }`}
> >
{theme === 'light' && <Sun size={18} />} {theme === 'light' && <Sun size={18} />}
@@ -256,15 +260,15 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
{/* Content Settings */} {/* Content Settings */}
<section> <section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Content Settings</h2> <h2 className="text-xl font-black text-foreground mb-6">Content Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100 space-y-4"> <div className="bg-muted/50 rounded-2xl p-6 border border-border space-y-4">
{/* Show adult content */} {/* Show adult content */}
<div className="flex items-center justify-between p-4 rounded-xl bg-white border border-zinc-100"> <div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border">
<div> <div>
<Label htmlFor="showAdult" className="text-sm font-black text-zinc-900 cursor-pointer"> <Label htmlFor="showAdult" className="text-sm font-black text-foreground cursor-pointer">
Show adult content Show adult content
</Label> </Label>
<p className="text-xs font-medium text-zinc-500 mt-1"> <p className="text-xs font-medium text-muted-foreground mt-1">
Display adult media in your library Display adult media in your library
</p> </p>
</div> </div>
@@ -276,12 +280,12 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
</div> </div>
{/* Auto-play trailers */} {/* Auto-play trailers */}
<div className="flex items-center justify-between p-4 rounded-xl bg-white border border-zinc-100"> <div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border">
<div> <div>
<Label htmlFor="autoPlay" className="text-sm font-black text-zinc-900 cursor-pointer"> <Label htmlFor="autoPlay" className="text-sm font-black text-foreground cursor-pointer">
Auto-play trailers Auto-play trailers
</Label> </Label>
<p className="text-xs font-medium text-zinc-500 mt-1"> <p className="text-xs font-medium text-muted-foreground mt-1">
Automatically play trailers when viewing media Automatically play trailers when viewing media
</p> </p>
</div> </div>
@@ -296,11 +300,11 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
{/* Language Settings */} {/* Language Settings */}
<section> <section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Language</h2> <h2 className="text-xl font-black text-foreground mb-6">Language</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100"> <div className="bg-muted/50 rounded-2xl p-6 border border-border">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Globe size={18} className="text-[#6d28d9]" /> <Globe size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-zinc-900">Interface language</Label> <Label className="text-sm font-black text-foreground">Interface language</Label>
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{LANGUAGE_OPTIONS.map((option) => ( {LANGUAGE_OPTIONS.map((option) => (
@@ -310,7 +314,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${ className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${
settings.language === option.value settings.language === option.value
? 'bg-[#6d28d9] text-white' ? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200' : 'bg-background text-foreground hover:bg-muted border border-border'
}`} }`}
> >
{option.label} {option.label}

View File

@@ -0,0 +1,74 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
effectiveTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
const stored = localStorage.getItem('theme') as Theme;
return stored || 'system';
});
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const root = window.document.documentElement;
const applyTheme = () => {
let resolved: 'light' | 'dark';
if (theme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} else {
resolved = theme;
}
setEffectiveTheme(resolved);
if (resolved === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
applyTheme();
// Listen for system theme changes when in system mode
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
applyTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}