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
+315 -227
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,
@@ -145,192 +160,260 @@ 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 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> <div>
<Link <h1 className="text-2xl font-bold text-foreground">Settings</h1>
to="/" <p className="text-sm text-muted-foreground">Manage your preferences</p>
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> </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 className="flex items-center gap-3">
<AnimatePresence mode="wait">
{saveStatus === 'success' && ( {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"> <motion.div
Settings saved successfully! initial={{ opacity: 0, x: 20 }}
</div> 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' && ( {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"> <motion.div
Failed to save settings. Please try again. initial={{ opacity: 0, x: 20 }}
</div> 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>
</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 */} {/* 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>
<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"> <Badge variant="secondary">{enabledCount}/{totalCategories} enabled</Badge>
<div className="flex items-center gap-4"> </div>
<div className="w-10 h-10 rounded-xl bg-muted flex items-center justify-center text-[#6d28d9] border border-border/30"> </CardHeader>
{CATEGORY_ICONS[category]} <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)}
>
<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>
<div> <div>
<Label htmlFor={category} className="text-sm font-black text-foreground cursor-pointer"> <p className="font-medium text-foreground">{category}</p>
{category} <p className="text-xs text-muted-foreground">
</Label> {isEnabled ? 'Visible in library' : 'Hidden'}
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
{settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'}
</p> </p>
</div> </div>
</div> </div>
<Switch <Switch
id={category} checked={isEnabled}
checked={settings.enabledCategories.includes(category)}
onCheckedChange={() => toggleCategory(category)} onCheckedChange={() => toggleCategory(category)}
/> />
</div> </div>
))} );
})}
</div> </div>
</div> </CardContent>
</section> </Card>
</TabsContent>
{/* Display Settings */} {/* Display Settings */}
<section> <TabsContent value="display" className="mt-0 space-y-6">
<h2 className="text-2xl font-black text-foreground mb-6">Display Settings</h2> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-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 */} {/* Items per page */}
<div> <div className="space-y-3">
<Label className="text-sm font-black text-foreground mb-2 block">Items per page</Label> <Label>Items per page</Label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{ITEMS_PER_PAGE_OPTIONS.map((option) => ( {ITEMS_PER_PAGE_OPTIONS.map((option) => (
<button <Button
key={option} key={option}
variant={settings.itemsPerPage === option ? 'default' : 'outline'}
size="sm"
onClick={() => setSettings(prev => ({ ...prev, itemsPerPage: 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} {option}
</button> </Button>
))} ))}
</div> </div>
</div> </div>
<Separator />
{/* Default view */} {/* Default view */}
<div> <div className="space-y-3">
<Label className="text-sm font-black text-foreground mb-2 block">Default view</Label> <Label>Default view</Label>
<div className="flex gap-2"> <div className="grid grid-cols-2 gap-3">
<button <Button
variant={settings.defaultView === 'grid' ? 'default' : 'outline'}
className="justify-center gap-2"
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'grid' }))} 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} /> <LayoutGrid className="h-4 w-4" />
Grid Grid
</button> </Button>
<button <Button
variant={settings.defaultView === 'list' ? 'default' : 'outline'}
className="justify-center gap-2"
onClick={() => setSettings(prev => ({ ...prev, defaultView: 'list' }))} 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 className="h-4 w-4" />
List List
</button> </Button>
</div> </div>
</div> </div>
<Separator />
{/* Grid item size */} {/* Grid item size */}
<div> <div className="space-y-3">
<Label className="text-sm font-black text-foreground mb-2 block">Grid item size</Label> <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"> <div className="flex items-center gap-4">
<span className="text-xs font-bold text-muted-foreground">Small</span> <span className="text-xs text-muted-foreground">Small</span>
<input <Slider
type="range"
min="1"
max="10"
value={settings.gridItemSize} value={settings.gridItemSize}
onChange={(e) => setSettings(prev => ({ ...prev, gridItemSize: Number(e.target.value) }))} min={1}
className="flex-1 h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-[#6d28d9]" max={10}
onValueChange={(value) => setSettings(prev => ({ ...prev, gridItemSize: value }))}
className="flex-1"
/> />
<span className="text-xs font-bold text-muted-foreground">Large</span> <span className="text-xs text-muted-foreground">Large</span>
<span className="text-sm font-bold text-[#6d28d9] w-8 text-center">{settings.gridItemSize}</span>
</div> </div>
</div> </div>
</CardContent>
</Card>
{/* Theme */} <Card className="border-border/60">
<div> <CardHeader>
<Label className="text-sm font-black text-foreground mb-2 block">Theme</Label> <CardTitle className="flex items-center gap-2">
<div className="flex gap-2"> <Languages className="h-4 w-4 text-primary" />
{(['light', 'dark', 'system'] as const).map((theme) => ( Language
<button </CardTitle>
key={theme} <CardDescription>Interface language preference</CardDescription>
onClick={() => setSettings(prev => ({ ...prev, theme }))} </CardHeader>
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all ${ <CardContent>
settings.theme === theme <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20' {LANGUAGE_OPTIONS.map((option) => (
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30' <Button
}`} key={option.value}
variant={settings.language === option.value ? 'default' : 'outline'}
size="sm"
onClick={() => setSettings(prev => ({ ...prev, language: option.value }))}
className="justify-center"
> >
{theme === 'light' && <Sun size={18} />} {option.label}
{theme === 'dark' && <Moon size={18} />} </Button>
{theme === 'system' && <Monitor size={18} />}
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</button>
))} ))}
</div> </div>
</CardContent>
</Card>
</div> </div>
</div> </TabsContent>
</section>
{/* Content Settings */} {/* Content Settings */}
<section> <TabsContent value="content" className="mt-0 space-y-6">
<h2 className="text-2xl font-black text-foreground mb-6">Content Settings</h2> <Card className="border-border/60">
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-4"> <CardHeader>
{/* Show adult content */} <CardTitle>Content Preferences</CardTitle>
<div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border/50 hover:border-[#6d28d9]/30 transition-all"> <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> <div>
<Label htmlFor="showAdult" className="text-sm font-black text-foreground cursor-pointer"> <Label htmlFor="showAdult" className="cursor-pointer">Show adult content</Label>
Show adult content <p className="text-sm text-muted-foreground">Display adult media in your library</p>
</Label>
<p className="text-xs font-medium text-muted-foreground mt-1">
Display adult media in your library
</p>
</div> </div>
<Switch <Switch
id="showAdult" id="showAdult"
@@ -339,15 +422,10 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
/> />
</div> </div>
{/* Auto-play trailers */} <div className="flex items-center justify-between p-4 rounded-lg bg-muted/30 border border-border/50">
<div className="flex items-center justify-between p-4 rounded-xl bg-background border border-border/50 hover:border-[#6d28d9]/30 transition-all">
<div> <div>
<Label htmlFor="autoPlay" className="text-sm font-black text-foreground cursor-pointer"> <Label htmlFor="autoPlay" className="cursor-pointer">Auto-play trailers</Label>
Auto-play trailers <p className="text-sm text-muted-foreground">Automatically play trailers when viewing media</p>
</Label>
<p className="text-xs font-medium text-muted-foreground mt-1">
Automatically play trailers when viewing media
</p>
</div> </div>
<Switch <Switch
id="autoPlay" id="autoPlay"
@@ -355,70 +433,78 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
onCheckedChange={(checked) => setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))} onCheckedChange={(checked) => setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))}
/> />
</div> </div>
</div> </CardContent>
</section> </Card>
</TabsContent>
{/* Language Settings */} {/* Appearance Settings */}
<section> <TabsContent value="appearance" className="mt-0 space-y-6">
<h2 className="text-2xl font-black text-foreground mb-6">Language</h2> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50"> <Card className="border-border/60">
<div className="flex items-center gap-2 mb-4"> <CardHeader>
<Globe size={18} className="text-[#6d28d9]" /> <CardTitle className="flex items-center gap-2">
<Label className="text-sm font-black text-foreground">Interface language</Label> <Sparkles className="h-4 w-4 text-primary" />
</div> Theme
<div className="flex gap-2 flex-wrap"> </CardTitle>
{LANGUAGE_OPTIONS.map((option) => ( <CardDescription>Choose your preferred color scheme</CardDescription>
<button </CardHeader>
key={option.value} <CardContent>
onClick={() => setSettings(prev => ({ ...prev, language: option.value }))} <div className="grid grid-cols-3 gap-3">
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all ${ {([
settings.language === option.value { value: 'light' as const, icon: Sun, label: 'Light' },
? 'bg-gradient-to-br from-[#6d28d9] to-[#8b5cf6] text-white shadow-lg shadow-[#6d28d9]/20' { value: 'dark' as const, icon: Moon, label: 'Dark' },
: 'bg-background text-foreground hover:bg-muted border border-border/50 hover:border-[#6d28d9]/30' { 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 }))}
> >
{option.label} <Icon className="h-5 w-5" />
</button> <span className="text-xs">{label}</span>
</Button>
))} ))}
</div> </div>
</div> </CardContent>
</section> </Card>
{/* Page Settings */} <Card className="border-border/60">
<section> <CardHeader>
<h2 className="text-2xl font-black text-foreground mb-6">Page Settings</h2> <CardTitle className="flex items-center gap-2">
<div className="bg-muted/50 backdrop-blur-sm rounded-2xl p-6 border border-border/50 space-y-6"> <Type className="h-4 w-4 text-primary" />
{/* Page Title */} Page Title
<div> </CardTitle>
<div className="flex items-center gap-2 mb-2"> <CardDescription>Customize the page title</CardDescription>
<Type size={18} className="text-[#6d28d9]" /> </CardHeader>
<Label className="text-sm font-black text-foreground">Custom Page Title</Label> <CardContent className="space-y-4">
</div> <Input
<input
type="text"
value={pageTitle} value={pageTitle}
onChange={(e) => setPageTitle(e.target.value)} onChange={(e) => setPageTitle(e.target.value)}
placeholder="Leave empty for default title" 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"> <p className="text-xs text-muted-foreground">
Custom title for your page. Leave empty to use the default title. Custom title for your page. Leave empty to use the default title.
</p> </p>
</div> </CardContent>
</Card>
{/* Favicon Upload */} <Card className="border-border/60">
<div> <CardHeader>
<div className="flex items-center gap-2 mb-2"> <CardTitle className="flex items-center gap-2">
<Image size={18} className="text-[#6d28d9]" /> <ImageIcon className="h-4 w-4 text-primary" />
<Label className="text-sm font-black text-foreground">Favicon / Icon</Label> Favicon
</div> </CardTitle>
<CardDescription>Upload a custom favicon</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{faviconPreview && ( {faviconPreview && (
<div className="relative"> <div className="relative">
<img <img
src={faviconPreview} src={faviconPreview}
alt="Favicon preview" alt="Favicon preview"
className="w-16 h-16 rounded-xl object-cover border border-border/50" className="w-16 h-16 rounded-lg object-cover border border-border"
/> />
<button <button
onClick={handleRemoveFavicon} onClick={handleRemoveFavicon}
@@ -436,63 +522,65 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
className="hidden" className="hidden"
id="favicon-upload" id="favicon-upload"
/> />
<label <label htmlFor="favicon-upload">
htmlFor="favicon-upload" <Button variant="outline" className="cursor-pointer" asChild>
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" <span>{favicon ? 'Change favicon' : 'Upload favicon'}</span>
> </Button>
<Image size={16} />
{favicon ? 'Change favicon' : 'Upload favicon'}
</label> </label>
</div> </div>
</div> </div>
<p className="text-xs font-medium text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-3">
Upload a custom favicon or icon. The image will be converted to Base64 format. The image will be converted to Base64 format.
</p> </p>
</div> </CardContent>
</Card>
{/* Custom Colors */} <Card className="border-border/60 lg:col-span-2">
<div> <CardHeader>
<div className="flex items-center gap-2 mb-4"> <CardTitle className="flex items-center gap-2">
<Palette size={18} className="text-[#6d28d9]" /> <Settings2 className="h-4 w-4 text-primary" />
<Label className="text-sm font-black text-foreground">Custom Colors</Label> Custom Colors
</div> </CardTitle>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <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 Color' }, { key: 'primary', label: 'Primary' },
{ key: 'secondary', label: 'Secondary Color' }, { key: 'secondary', label: 'Secondary' },
{ key: 'background', label: 'Background Color' }, { key: 'background', label: 'Background' },
{ key: 'surface', label: 'Surface Color' }, { key: 'surface', label: 'Surface' },
{ key: 'text', label: 'Text Color' }, { key: 'text', label: 'Text' },
{ key: 'muted', label: 'Muted Text Color' }, { key: 'muted', label: 'Muted' },
{ key: 'border', label: 'Border Color' }, { key: 'border', label: 'Border' },
].map(({ key, label }) => ( ].map(({ key, label }) => (
<div key={key} className="flex items-center gap-3 p-3 rounded-xl bg-background border border-border/50"> <div key={key} className="space-y-2">
<Label className="text-xs">{label}</Label>
<div className="flex gap-2">
<input <input
type="color" type="color"
value={customColors[key as keyof CustomColors] || '#6d28d9'} value={customColors[key as keyof CustomColors] || '#6d28d9'}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)} onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
className="w-10 h-10 rounded-lg cursor-pointer border-0" className="w-10 h-10 rounded-lg cursor-pointer border-0 p-0"
/> />
<div className="flex-1"> <Input
<Label className="text-xs font-black text-foreground">{label}</Label>
<input
type="text"
value={customColors[key as keyof CustomColors] || ''} value={customColors[key as keyof CustomColors] || ''}
onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)} onChange={(e) => handleColorChange(key as keyof CustomColors, e.target.value)}
placeholder="#6d28d9" 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" className="flex-1 text-xs"
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
<p className="text-xs font-medium text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-4">
Leave color fields empty to use the default theme colors. Leave color fields empty to use the default theme colors.
</p> </p>
</CardContent>
</Card>
</div> </div>
</div> </TabsContent>
</section> </Tabs>
</div>
</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 }