Refactor Settings UI into tabs and add slider

Rework SettingsView into a tabbed, card-based layout with improved controls and UX. Adds save status indicators/animations, a back navigation button (useNavigate), badges, separators, and consistent Button/Input/Card/Tabs components. Refactors CATEGORY_ICONS to use React.ElementType and updates icon imports (BookOpen, ImageIcon, etc.). Introduces a new Slider component (src/components/ui/slider.tsx) and replaces the old range input for grid item size with the new Slider. Also consolidates custom color inputs, favicon upload UI, language selection, and other display/content controls into structured cards for clarity.
This commit is contained in:
Lars Behrends
2026-04-26 02:22:09 +02:00
parent 073c8a6c5d
commit 4605b251be
2 changed files with 475 additions and 347 deletions
+435 -347
View File
@@ -2,20 +2,33 @@ import React, { useState, useEffect } from 'react';
import { MediaCategory, UserSettings, CustomColors } from '@/types'; import { MediaCategory, UserSettings, CustomColors } from '@/types';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Film, Music, Book, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon, Save, ArrowLeft, Type, Image, Palette } from 'lucide-react'; import { Input } from '@/components/ui/input';
import { Link } from 'react-router-dom'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Slider } from '@/components/ui/slider';
import {
Film, Music, BookOpen, Tv, Gamepad2, ShieldAlert, LayoutGrid, List, Globe, Monitor, Sun, Moon,
Save, ArrowLeft, Type, Image as ImageIcon, Palette, Library, Eye, Sparkles, Languages, Settings2,
Check, AlertCircle, MonitorPlay
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { fetchSettings, updateSettings } from '@/api'; import { fetchSettings, updateSettings } from '@/api';
import { useTheme } from '@/contexts/ThemeContext'; import { useTheme } from '@/contexts/ThemeContext';
import { motion, AnimatePresence } from 'motion/react';
import { cn } from '@/lib/utils';
const CATEGORY_ICONS: Record<MediaCategory, React.ReactNode> = { const CATEGORY_ICONS: Record<MediaCategory, React.ElementType> = {
Anime: <Tv size={18} />, Anime: Tv,
Movies: <Film size={18} />, Movies: Film,
'TV Series': <Tv size={18} />, 'TV Series': Tv,
Music: <Music size={18} />, Music: Music,
Books: <Book size={18} />, Books: BookOpen,
Consoles: <Gamepad2 size={18} />, Consoles: Gamepad2,
Games: <Gamepad2 size={18} />, Games: Gamepad2,
Adult: <ShieldAlert size={18} />, Adult: ShieldAlert,
}; };
const ITEMS_PER_PAGE_OPTIONS = [12, 20, 36, 48, 60]; const ITEMS_PER_PAGE_OPTIONS = [12, 20, 36, 48, 60];
@@ -32,7 +45,9 @@ interface SettingsViewProps {
} }
export default function SettingsView({ onSettingsSaved }: SettingsViewProps) { export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const navigate = useNavigate();
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const [activeTab, setActiveTab] = useState('library');
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,
@@ -46,7 +61,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
// Page Settings State // Page Settings State
const [pageTitle, setPageTitle] = useState<string>(''); const [pageTitle, setPageTitle] = useState<string>('');
const [favicon, setFavicon] = useState<string>(''); const [favicon, setFavicon] = useState<string>('');
@@ -145,354 +160,427 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
); );
} }
const enabledCount = settings.enabledCategories.length;
const totalCategories = 8;
return ( return (
<div className="min-h-screen bg-background pt-20"> <div className="min-h-screen bg-background pb-16">
{/* Content */} {/* Header */}
<div className="max-w-[1920px] mx-auto px-6 py-12"> <div className="border-b border-border/50">
<div className="flex items-center justify-between mb-8"> <div className="max-w-[1920px] mx-auto px-4 sm:px-6 py-6">
<div> <div className="flex items-center justify-between">
<Link <div className="flex items-center gap-4">
to="/" <Button
className="inline-flex items-center gap-2 text-sm font-bold text-muted-foreground hover:text-[#6d28d9] transition-colors mb-2 hover:bg-muted/50 px-3 py-1 rounded-xl transition-all duration-300" variant="ghost"
> size="icon"
<ArrowLeft size={16} /> onClick={() => navigate(-1)}
Back to home className="rounded-lg"
</Link> >
<h1 className="text-4xl font-black text-foreground">Settings</h1> <ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="text-sm text-muted-foreground">Manage your preferences</p>
</div>
</div>
<div className="flex items-center gap-3">
<AnimatePresence mode="wait">
{saveStatus === 'success' && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="flex items-center gap-2 text-sm text-emerald-500 bg-emerald-500/10 px-3 py-1.5 rounded-lg"
>
<Check className="h-4 w-4" />
Saved
</motion.div>
)}
{saveStatus === 'error' && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="flex items-center gap-2 text-sm text-red-500 bg-red-500/10 px-3 py-1.5 rounded-lg"
>
<AlertCircle className="h-4 w-4" />
Error
</motion.div>
)}
</AnimatePresence>
<Button
onClick={handleSave}
disabled={isSaving}
className="gap-2"
>
<Save className="h-4 w-4" />
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div> </div>
<button
onClick={handleSave}
disabled={isSaving}
className="bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] hover:from-[#5b21b6] hover:to-[#7c3aed] text-white font-bold px-6 py-3 h-12 rounded-xl flex items-center gap-2 transition-all duration-300 hover:scale-[1.02] shadow-lg shadow-[#6d28d9]/30 disabled:opacity-50 disabled:hover:scale-100"
>
{isSaving ? (
'Saving...'
) : (
<>
<Save size={16} />
Save Changes
</>
)}
</button>
</div> </div>
</div>
{saveStatus === 'success' && ( {/* Content */}
<div className="mb-6 p-4 bg-green-500/10 border border-green-500/30 rounded-xl text-green-500 font-medium backdrop-blur-sm"> <div className="max-w-[1920px] mx-auto px-4 sm:px-6 py-6">
Settings saved successfully! <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
</div> <TabsList className="mb-6 w-full justify-start bg-muted/50 p-1 rounded-lg h-auto flex-wrap">
)} <TabsTrigger value="library" className="gap-2">
{saveStatus === 'error' && ( <Library className="h-4 w-4" />
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-xl text-red-500 font-medium backdrop-blur-sm"> Library
Failed to save settings. Please try again. </TabsTrigger>
</div> <TabsTrigger value="display" className="gap-2">
)} <Monitor className="h-4 w-4" />
Display
</TabsTrigger>
<TabsTrigger value="content" className="gap-2">
<Eye className="h-4 w-4" />
Content
</TabsTrigger>
<TabsTrigger value="appearance" className="gap-2">
<Palette className="h-4 w-4" />
Appearance
</TabsTrigger>
</TabsList>
<div className="grid gap-8">
{/* Library Settings */} {/* Library Settings */}
<section> <TabsContent value="library" className="mt-0 space-y-6">
<h2 className="text-2xl font-black text-foreground mb-6">Library Settings</h2> <Card className="border-border/60">
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50"> <CardHeader>
<p className="text-sm font-medium text-muted-foreground mb-4"> <div className="flex items-center justify-between">
Toggle which media areas you want to see in your library. <div>
</p> <CardTitle>Media Categories</CardTitle>
<div className="grid gap-4"> <CardDescription>Toggle which media types appear in your library</CardDescription>
{(['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-background border border-border/50 transition-all hover:border-[#6d28d9]/30 hover:bg-muted/50">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-muted flex items-center justify-center text-[#6d28d9] border border-border/30">
{CATEGORY_ICONS[category]}
</div>
<div>
<Label htmlFor={category} className="text-sm font-black text-foreground cursor-pointer">
{category}
</Label>
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'}
</p>
</div>
</div>
<Switch
id={category}
checked={settings.enabledCategories.includes(category)}
onCheckedChange={() => toggleCategory(category)}
/>
</div> </div>
))} <Badge variant="secondary">{enabledCount}/{totalCategories} enabled</Badge>
</div>
</div>
</section>
{/* Display Settings */}
<section>
<h2 className="text-2xl font-black text-foreground mb-6">Display Settings</h2>
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-6">
{/* Items per page */}
<div>
<Label className="text-sm font-black text-foreground mb-2 block">Items per page</Label>
<div className="flex gap-2 flex-wrap">
{ITEMS_PER_PAGE_OPTIONS.map((option) => (
<button
key={option}
onClick={() => setSettings(prev => ({ ...prev, itemsPerPage: option }))}
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
settings.itemsPerPage === option
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20'
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30'
}`}
>
{option}
</button>
))}
</div> </div>
</div> </CardHeader>
<CardContent>
{/* Default view */} <div className="grid gap-3">
<div> {(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => {
<Label className="text-sm font-black text-foreground mb-2 block">Default view</Label> const Icon = CATEGORY_ICONS[category];
<div className="flex gap-2"> const isEnabled = settings.enabledCategories.includes(category);
<button return (
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))} <div
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all ${ key={category}
settings.defaultView === 'grid' className={cn(
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20' "flex items-center justify-between p-4 rounded-lg border transition-all cursor-pointer",
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30' isEnabled
}`} ? "bg-background border-primary/30"
> : "bg-muted/30 border-border/50 opacity-60"
<LayoutGrid size={18} /> )}
Grid onClick={() => toggleCategory(category)}
</button>
<button
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'list' }))}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all ${
settings.defaultView === 'list'
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20'
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30'
}`}
>
<List size={18} />
List
</button>
</div>
</div>
{/* Grid item size */}
<div>
<Label className="text-sm font-black text-foreground mb-2 block">Grid item size</Label>
<div className="flex items-center gap-4">
<span className="text-xs font-bold text-muted-foreground">Small</span>
<input
type="range"
min="1"
max="10"
value={settings.gridItemSize}
onChange={(e) => setSettings(prev => ({ ...prev, gridItemSize: Number(e.target.value) }))}
className="flex-1 h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-[#6d28d9]"
/>
<span className="text-xs font-bold text-muted-foreground">Large</span>
<span className="text-sm font-bold text-[#6d28d9] w-8 text-center">{settings.gridItemSize}</span>
</div>
</div>
{/* Theme */}
<div>
<Label className="text-sm font-black text-foreground mb-2 block">Theme</Label>
<div className="flex gap-2">
{(['light', 'dark', 'system'] as const).map((theme) => (
<button
key={theme}
onClick={() => setSettings(prev => ({ ...prev, theme }))}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all ${
settings.theme === theme
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20'
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30'
}`}
>
{theme === 'light' && <Sun size={18} />}
{theme === 'dark' && <Moon size={18} />}
{theme === 'system' && <Monitor size={18} />}
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</button>
))}
</div>
</div>
</div>
</section>
{/* Content Settings */}
<section>
<h2 className="text-2xl font-black text-foreground mb-6">Content Settings</h2>
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-4">
{/* Show adult content */}
<div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border/50 hover:border-[#6d28d9]/30 transition-all">
<div>
<Label htmlFor="showAdult" className="text-sm font-black text-foreground cursor-pointer">
Show adult content
</Label>
<p className="text-xs font-medium text-muted-foreground mt-1">
Display adult media in your library
</p>
</div>
<Switch
id="showAdult"
checked={settings.showAdultContent}
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, showAdultContent: checked }))}
/>
</div>
{/* Auto-play trailers */}
<div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border/50 hover:border-[#6d28d9]/30 transition-all">
<div>
<Label htmlFor="autoPlay" className="text-sm font-black text-foreground cursor-pointer">
Auto-play trailers
</Label>
<p className="text-xs font-medium text-muted-foreground mt-1">
Automatically play trailers when viewing media
</p>
</div>
<Switch
id="autoPlay"
checked={settings.autoPlayTrailers}
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))}
/>
</div>
</div>
</section>
{/* Language Settings */}
<section>
<h2 className="text-2xl font-black text-foreground mb-6">Language</h2>
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50">
<div className="flex items-center gap-2 mb-4">
<Globe size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Interface language</Label>
</div>
<div className="flex gap-2 flex-wrap">
{LANGUAGE_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => setSettings(prev => ({ ...prev, language: option.value }))}
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${
settings.language === option.value
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20'
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30'
}`}
>
{option.label}
</button>
))}
</div>
</div>
</section>
{/* Page Settings */}
<section>
<h2 className="text-2xl font-black text-foreground mb-6">Page Settings</h2>
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-6">
{/* Page Title */}
<div>
<div className="flex items-center gap-2 mb-2">
<Type size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Custom Page Title</Label>
</div>
<input
type="text"
value={pageTitle}
onChange={(e) => setPageTitle(e.target.value)}
placeholder="Leave empty for default title"
className="w-full px-4 py-3 rounded-xl bg-background border border-border/50 text-foreground placeholder:text-muted-foreground/50 focus:border-[#6d28d9] focus:outline-none transition-all"
/>
<p className="text-xs font-medium text-muted-foreground mt-2">
Custom title for your page. Leave empty to use the default title.
</p>
</div>
{/* Favicon Upload */}
<div>
<div className="flex items-center gap-2 mb-2">
<Image size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Favicon / Icon</Label>
</div>
<div className="flex items-center gap-4">
{faviconPreview && (
<div className="relative">
<img
src={faviconPreview}
alt="Favicon preview"
className="w-16 h-16 rounded-xl object-cover border border-border/50"
/>
<button
onClick={handleRemoveFavicon}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors"
> >
× <div className="flex items-center gap-3">
</button> <div className={cn(
</div> "w-10 h-10 rounded-lg flex items-center justify-center border",
)} isEnabled
<div className="flex-1"> ? "bg-primary/10 text-primary border-primary/20"
<input : "bg-muted text-muted-foreground border-border"
type="file" )}>
accept="image/*" <Icon className="h-5 w-5" />
onChange={handleFaviconUpload} </div>
className="hidden" <div>
id="favicon-upload" <p className="font-medium text-foreground">{category}</p>
/> <p className="text-xs text-muted-foreground">
<label {isEnabled ? 'Visible in library' : 'Hidden'}
htmlFor="favicon-upload" </p>
className="inline-flex items-center gap-2 px-4 py-3 rounded-xl bg-background border border-border/50 text-foreground hover:bg-muted hover:border-[#6d28d9]/30 cursor-pointer transition-all" </div>
> </div>
<Image size={16} /> <Switch
{favicon ? 'Change favicon' : 'Upload favicon'} checked={isEnabled}
</label> onCheckedChange={() => toggleCategory(category)}
</div>
</div>
<p className="text-xs font-medium text-muted-foreground mt-2">
Upload a custom favicon or icon. The image will be converted to Base64 format.
</p>
</div>
{/* Custom Colors */}
<div>
<div className="flex items-center gap-2 mb-4">
<Palette size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-foreground">Custom Colors</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[
{ key: 'primary', label: 'Primary Color' },
{ key: 'secondary', label: 'Secondary Color' },
{ key: 'background', label: 'Background Color' },
{ key: 'surface', label: 'Surface Color' },
{ key: 'text', label: 'Text Color' },
{ key: 'muted', label: 'Muted Text Color' },
{ key: 'border', label: 'Border Color' },
].map(({ key, label }) => (
<div key={key} className="flex items-center gap-3 p-3 rounded-xl bg-background border border-border/50">
<input
type="color"
value={customColors[key as keyof CustomColors] || '#6d28d9'}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
className="w-10 h-10 rounded-lg cursor-pointer border-0"
/>
<div className="flex-1">
<Label className="text-xs font-black text-foreground">{label}</Label>
<input
type="text"
value={customColors[key as keyof CustomColors] || ''}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
placeholder="#6d28d9"
className="w-full mt-1 px-2 py-1 rounded-lg bg-muted border border-border/30 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-[#6d28d9] focus:outline-none transition-all"
/> />
</div> </div>
</div> );
))} })}
</div> </div>
<p className="text-xs font-medium text-muted-foreground mt-2"> </CardContent>
Leave color fields empty to use the default theme colors. </Card>
</p> </TabsContent>
</div>
{/* Display Settings */}
<TabsContent value="display" className="mt-0 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="border-border/60">
<CardHeader>
<CardTitle>View Options</CardTitle>
<CardDescription>Configure how items are displayed</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Items per page */}
<div className="space-y-3">
<Label>Items per page</Label>
<div className="flex gap-2 flex-wrap">
{ITEMS_PER_PAGE_OPTIONS.map((option) => (
<Button
key={option}
variant={settings.itemsPerPage === option ? 'default' : 'outline'}
size="sm"
onClick={() => setSettings(prev => ({ ...prev, itemsPerPage: option }))}
>
{option}
</Button>
))}
</div>
</div>
<Separator />
{/* Default view */}
<div className="space-y-3">
<Label>Default view</Label>
<div className="grid grid-cols-2 gap-3">
<Button
variant={settings.defaultView === 'grid' ? 'default' : 'outline'}
className="justify-center gap-2"
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))}
>
<LayoutGrid className="h-4 w-4" />
Grid
</Button>
<Button
variant={settings.defaultView === 'list' ? 'default' : 'outline'}
className="justify-center gap-2"
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'list' }))}
>
<List className="h-4 w-4" />
List
</Button>
</div>
</div>
<Separator />
{/* Grid item size */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Grid item size</Label>
<span className="text-sm font-medium text-primary">{settings.gridItemSize}</span>
</div>
<div className="flex items-center gap-4">
<span className="text-xs text-muted-foreground">Small</span>
<Slider
value={settings.gridItemSize}
min={1}
max={10}
onValueChange={(value) => setSettings(prev => ({ ...prev, gridItemSize: value }))}
className="flex-1"
/>
<span className="text-xs text-muted-foreground">Large</span>
</div>
</div>
</CardContent>
</Card>
<Card className="border-border/60">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Languages className="h-4 w-4 text-primary" />
Language
</CardTitle>
<CardDescription>Interface language preference</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{LANGUAGE_OPTIONS.map((option) => (
<Button
key={option.value}
variant={settings.language === option.value ? 'default' : 'outline'}
size="sm"
onClick={() => setSettings(prev => ({ ...prev, language: option.value }))}
className="justify-center"
>
{option.label}
</Button>
))}
</div>
</CardContent>
</Card>
</div> </div>
</section> </TabsContent>
</div>
{/* Content Settings */}
<TabsContent value="content" className="mt-0 space-y-6">
<Card className="border-border/60">
<CardHeader>
<CardTitle>Content Preferences</CardTitle>
<CardDescription>Control what content is shown</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/30 border border-border/50">
<div>
<Label htmlFor="showAdult" className="cursor-pointer">Show adult content</Label>
<p className="text-sm text-muted-foreground">Display adult media in your library</p>
</div>
<Switch
id="showAdult"
checked={settings.showAdultContent}
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, showAdultContent: checked }))}
/>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/30 border border-border/50">
<div>
<Label htmlFor="autoPlay" className="cursor-pointer">Auto-play trailers</Label>
<p className="text-sm text-muted-foreground">Automatically play trailers when viewing media</p>
</div>
<Switch
id="autoPlay"
checked={settings.autoPlayTrailers}
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))}
/>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Appearance Settings */}
<TabsContent value="appearance" className="mt-0 space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="border-border/60">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" />
Theme
</CardTitle>
<CardDescription>Choose your preferred color scheme</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-3">
{([
{ value: 'light' as const, icon: Sun, label: 'Light' },
{ value: 'dark' as const, icon: Moon, label: 'Dark' },
{ value: 'system' as const, icon: Monitor, label: 'System' },
]).map(({ value, icon: Icon, label }) => (
<Button
key={value}
variant={settings.theme === value ? 'default' : 'outline'}
className="flex-col gap-2 h-auto py-4"
onClick={() => setSettings(prev => ({ ...prev, theme: value }))}
>
<Icon className="h-5 w-5" />
<span className="text-xs">{label}</span>
</Button>
))}
</div>
</CardContent>
</Card>
<Card className="border-border/60">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Type className="h-4 w-4 text-primary" />
Page Title
</CardTitle>
<CardDescription>Customize the page title</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Input
value={pageTitle}
onChange={(e) => setPageTitle(e.target.value)}
placeholder="Leave empty for default title"
/>
<p className="text-xs text-muted-foreground">
Custom title for your page. Leave empty to use the default title.
</p>
</CardContent>
</Card>
<Card className="border-border/60">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ImageIcon className="h-4 w-4 text-primary" />
Favicon
</CardTitle>
<CardDescription>Upload a custom favicon</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
{faviconPreview && (
<div className="relative">
<img
src={faviconPreview}
alt="Favicon preview"
className="w-16 h-16 rounded-lg object-cover border border-border"
/>
<button
onClick={handleRemoveFavicon}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors"
>
×
</button>
</div>
)}
<div className="flex-1">
<input
type="file"
accept="image/*"
onChange={handleFaviconUpload}
className="hidden"
id="favicon-upload"
/>
<label htmlFor="favicon-upload">
<Button variant="outline" className="cursor-pointer" asChild>
<span>{favicon ? 'Change favicon' : 'Upload favicon'}</span>
</Button>
</label>
</div>
</div>
<p className="text-xs text-muted-foreground mt-3">
The image will be converted to Base64 format.
</p>
</CardContent>
</Card>
<Card className="border-border/60 lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-primary" />
Custom Colors
</CardTitle>
<CardDescription>Customize the application colors</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-4">
{[
{ key: 'primary', label: 'Primary' },
{ key: 'secondary', label: 'Secondary' },
{ key: 'background', label: 'Background' },
{ key: 'surface', label: 'Surface' },
{ key: 'text', label: 'Text' },
{ key: 'muted', label: 'Muted' },
{ key: 'border', label: 'Border' },
].map(({ key, label }) => (
<div key={key} className="space-y-2">
<Label className="text-xs">{label}</Label>
<div className="flex gap-2">
<input
type="color"
value={customColors[key as keyof CustomColors] || '#6d28d9'}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
className="w-10 h-10 rounded-lg cursor-pointer border-0 p-0"
/>
<Input
value={customColors[key as keyof CustomColors] || ''}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
placeholder="#6d28d9"
className="flex-1 text-xs"
/>
</div>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-4">
Leave color fields empty to use the default theme colors.
</p>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div> </div>
</div> </div>
); );
+40
View File
@@ -0,0 +1,40 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SliderProps extends React.InputHTMLAttributes<HTMLInputElement> {
value?: number
min?: number
max?: number
step?: number
onValueChange?: (value: number) => void
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
({ className, value, min = 0, max = 100, step = 1, onValueChange, onChange, ...props }, ref) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value)
onValueChange?.(newValue)
onChange?.(e)
}
return (
<input
type="range"
ref={ref}
value={value}
min={min}
max={max}
step={step}
onChange={handleChange}
className={cn(
"w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary",
className
)}
{...props}
/>
)
}
)
Slider.displayName = "Slider"
export { Slider }