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:
+434
-346
@@ -2,20 +2,33 @@ import React, { useState, useEffect } from 'react';
|
||||
import { MediaCategory, UserSettings, CustomColors } from '@/types';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
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 { Link } from 'react-router-dom';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 { useTheme } from '@/contexts/ThemeContext';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const CATEGORY_ICONS: Record<MediaCategory, React.ReactNode> = {
|
||||
Anime: <Tv size={18} />,
|
||||
Movies: <Film size={18} />,
|
||||
'TV Series': <Tv size={18} />,
|
||||
Music: <Music size={18} />,
|
||||
Books: <Book size={18} />,
|
||||
Consoles: <Gamepad2 size={18} />,
|
||||
Games: <Gamepad2 size={18} />,
|
||||
Adult: <ShieldAlert size={18} />,
|
||||
const CATEGORY_ICONS: Record<MediaCategory, React.ElementType> = {
|
||||
Anime: Tv,
|
||||
Movies: Film,
|
||||
'TV Series': Tv,
|
||||
Music: Music,
|
||||
Books: BookOpen,
|
||||
Consoles: Gamepad2,
|
||||
Games: Gamepad2,
|
||||
Adult: ShieldAlert,
|
||||
};
|
||||
|
||||
const ITEMS_PER_PAGE_OPTIONS = [12, 20, 36, 48, 60];
|
||||
@@ -32,7 +45,9 @@ interface SettingsViewProps {
|
||||
}
|
||||
|
||||
export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
const navigate = useNavigate();
|
||||
const { setTheme } = useTheme();
|
||||
const [activeTab, setActiveTab] = useState('library');
|
||||
const [settings, setSettings] = useState<UserSettings>({
|
||||
enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'],
|
||||
itemsPerPage: 20,
|
||||
@@ -145,354 +160,427 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const enabledCount = settings.enabledCategories.length;
|
||||
const totalCategories = 8;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pt-20">
|
||||
{/* Content */}
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-12">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link
|
||||
to="/"
|
||||
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"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Back to home
|
||||
</Link>
|
||||
<h1 className="text-4xl font-black text-foreground">Settings</h1>
|
||||
<div className="min-h-screen bg-background pb-16">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border/50">
|
||||
<div className="max-w-[1920px] mx-auto px-4 sm:px-6 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(-1)}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{saveStatus === 'success' && (
|
||||
<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">
|
||||
Settings saved successfully!
|
||||
</div>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<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">
|
||||
Failed to save settings. Please try again.
|
||||
</div>
|
||||
)}
|
||||
{/* Content */}
|
||||
<div className="max-w-[1920px] mx-auto px-4 sm:px-6 py-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<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">
|
||||
<Library className="h-4 w-4" />
|
||||
Library
|
||||
</TabsTrigger>
|
||||
<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 */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-black text-foreground mb-6">Library Settings</h2>
|
||||
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-4">
|
||||
Toggle which media areas you want to see in your library.
|
||||
</p>
|
||||
<div className="grid gap-4">
|
||||
{(['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)}
|
||||
/>
|
||||
<TabsContent value="library" className="mt-0 space-y-6">
|
||||
<Card className="border-border/60">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Media Categories</CardTitle>
|
||||
<CardDescription>Toggle which media types appear in your library</CardDescription>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
))}
|
||||
<Badge variant="secondary">{enabledCount}/{totalCategories} enabled</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default view */}
|
||||
<div>
|
||||
<Label className="text-sm font-black text-foreground mb-2 block">Default view</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all ${
|
||||
settings.defaultView === 'grid'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<LayoutGrid size={18} />
|
||||
Grid
|
||||
</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"
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-3">
|
||||
{(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => {
|
||||
const Icon = CATEGORY_ICONS[category];
|
||||
const isEnabled = settings.enabledCategories.includes(category);
|
||||
return (
|
||||
<div
|
||||
key={category}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-4 rounded-lg border transition-all cursor-pointer",
|
||||
isEnabled
|
||||
? "bg-background border-primary/30"
|
||||
: "bg-muted/30 border-border/50 opacity-60"
|
||||
)}
|
||||
onClick={() => toggleCategory(category)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFaviconUpload}
|
||||
className="hidden"
|
||||
id="favicon-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="favicon-upload"
|
||||
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"
|
||||
>
|
||||
<Image size={16} />
|
||||
{favicon ? 'Change favicon' : 'Upload favicon'}
|
||||
</label>
|
||||
</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 className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-10 h-10 rounded-lg flex items-center justify-center border",
|
||||
isEnabled
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted text-muted-foreground border-border"
|
||||
)}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{category}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isEnabled ? 'Visible in library' : 'Hidden'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={() => toggleCategory(category)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground mt-2">
|
||||
Leave color fields empty to use the default theme colors.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 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>
|
||||
</section>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user