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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user