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

@@ -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="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-12">
<div>
<h1 className="text-4xl font-black text-zinc-900 mb-2">Cast & Staff</h1>
<p className="text-zinc-500 font-medium">Discover the people behind your favorite media</p>
<h1 className="text-4xl font-black text-foreground mb-2">Cast & Staff</h1>
<p className="text-muted-foreground font-medium">Discover the people behind your favorite media</p>
</div>
<div className="flex items-center gap-4">
<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
placeholder="Search cast..."
value={searchQuery}
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>
<Button
variant={showFilters ? 'default' : 'outline'}
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)}
>
<Filter size={20} />
@@ -202,7 +202,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<Button
variant="outline"
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')}
>
<ArrowUpDown size={20} />
@@ -211,7 +211,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<Button
variant="ghost"
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}
title="Reset filters"
>
@@ -226,15 +226,15 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
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>
<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
value={sortBy}
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="role">Role</option>
@@ -243,11 +243,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
</select>
</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
value={filterOccupation}
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>
{uniqueOccupations.map(occ => (
@@ -256,11 +256,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
</select>
</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
value={filterMediaType}
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>
{uniqueMediaTypes.map(type => (
@@ -273,7 +273,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{searchQuery && (
<Badge variant="secondary" className="gap-1">
Search: {searchQuery}
<button onClick={() => setSearchQuery('')} className="hover:text-zinc-900">
<button onClick={() => setSearchQuery('')} className="hover:text-foreground">
<X size={12} />
</button>
</Badge>
@@ -281,7 +281,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{filterOccupation && (
<Badge variant="secondary" className="gap-1">
Occupation: {filterOccupation}
<button onClick={() => setFilterOccupation('')} className="hover:text-zinc-900">
<button onClick={() => setFilterOccupation('')} className="hover:text-foreground">
<X size={12} />
</button>
</Badge>
@@ -289,7 +289,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{filterMediaType && (
<Badge variant="secondary" className="gap-1">
Media Type: {filterMediaType}
<button onClick={() => setFilterMediaType('')} className="hover:text-zinc-900">
<button onClick={() => setFilterMediaType('')} className="hover:text-foreground">
<X size={12} />
</button>
</Badge>
@@ -297,7 +297,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{(sortBy !== 'name' || sortOrder !== 'asc') && (
<Badge variant="secondary" className="gap-1">
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} />
</button>
</Badge>
@@ -307,12 +307,12 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
)}
{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" />
<p className="text-lg font-bold">Loading cast...</p>
</div>
) : 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" />
<p className="text-lg font-bold">No cast members found</p>
</div>
@@ -326,11 +326,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
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)}
>
<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
src={person.photo}
alt={person.name}
@@ -339,18 +339,18 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
/>
</div>
<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}
</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}
</p>
</div>
</div>
{person.filmography && person.filmography.length > 0 && (
<div className="bg-zinc-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="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-background">
<img
src={person.filmography[0].poster || person.photo}
alt={person.filmography[0].title}
@@ -359,8 +359,8 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
/>
</div>
<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-xs font-bold text-zinc-700 truncate">{person.filmography[0].title}</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-foreground truncate">{person.filmography[0].title}</p>
<p className="text-[10px] text-[#6d28d9] font-bold truncate mt-1">{person.filmography[0].role}</p>
</div>
</div>
@@ -373,15 +373,15 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
{/* Pagination Controls */}
{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">
<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
value={itemsPerPage}
onChange={(e) => {
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 => (
<option key={size} value={size}>{size}</option>
@@ -395,7 +395,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
size="sm"
onClick={handlePrevPage}
disabled={currentPage === 1}
className="gap-2 font-bold border-zinc-200"
className="gap-2 font-bold border-border"
>
<ChevronLeft size={16} />
Previous
@@ -403,8 +403,8 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
<div className="flex items-center gap-2">
<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 font-bold text-zinc-700">{totalPages || 1}</span>
<span className="text-sm text-muted-foreground font-medium">of</span>
<span className="text-sm font-bold text-foreground">{totalPages || 1}</span>
</div>
<Button
@@ -412,7 +412,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
size="sm"
onClick={handleNextPage}
disabled={currentPage === totalPages || totalPages === 0}
className="gap-2 font-bold border-zinc-200"
className="gap-2 font-bold border-border"
>
Next
<ChevronRight size={16} />