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 { 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,
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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