From 4605b251be31ffa6e804a739f2bf33ee0dcaf531 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Sun, 26 Apr 2026 02:22:09 +0200 Subject: [PATCH] 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. --- src/components/SettingsView.tsx | 782 ++++++++++++++++++-------------- src/components/ui/slider.tsx | 40 ++ 2 files changed, 475 insertions(+), 347 deletions(-) create mode 100644 src/components/ui/slider.tsx diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index 1d03d77..60bade9 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -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 = { - Anime: , - Movies: , - 'TV Series': , - Music: , - Books: , - Consoles: , - Games: , - Adult: , +const CATEGORY_ICONS: Record = { + 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({ enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'], itemsPerPage: 20, @@ -46,7 +61,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) { const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); - + // Page Settings State const [pageTitle, setPageTitle] = useState(''); const [favicon, setFavicon] = useState(''); @@ -145,354 +160,427 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) { ); } + const enabledCount = settings.enabledCategories.length; + const totalCategories = 8; + return ( -
- {/* Content */} -
-
-
- - - Back to home - -

Settings

+
+ {/* Header */} +
+
+
+
+ +
+

Settings

+

Manage your preferences

+
+
+ +
+ + {saveStatus === 'success' && ( + + + Saved + + )} + {saveStatus === 'error' && ( + + + Error + + )} + + + +
-
+
- {saveStatus === 'success' && ( -
- Settings saved successfully! -
- )} - {saveStatus === 'error' && ( -
- Failed to save settings. Please try again. -
- )} + {/* Content */} +
+ + + + + Library + + + + Display + + + + Content + + + + Appearance + + -
{/* Library Settings */} -
-

Library Settings

-
-

- Toggle which media areas you want to see in your library. -

-
- {(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => ( -
-
-
- {CATEGORY_ICONS[category]} -
-
- -

- {settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'} -

-
-
- toggleCategory(category)} - /> + + + +
+
+ Media Categories + Toggle which media types appear in your library
- ))} -
-
-
- - {/* Display Settings */} -
-

Display Settings

-
- {/* Items per page */} -
- -
- {ITEMS_PER_PAGE_OPTIONS.map((option) => ( - - ))} + {enabledCount}/{totalCategories} enabled
-
- - {/* Default view */} -
- -
- - -
-
- - {/* Grid item size */} -
- -
- Small - setSettings(prev => ({ ...prev, gridItemSize: Number(e.target.value) }))} - className="flex-1 h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-[#6d28d9]" - /> - Large - {settings.gridItemSize} -
-
- - {/* Theme */} -
- -
- {(['light', 'dark', 'system'] as const).map((theme) => ( - - ))} -
-
-
-
- - {/* Content Settings */} -
-

Content Settings

-
- {/* Show adult content */} -
-
- -

- Display adult media in your library -

-
- setSettings(prev => ({ ...prev, showAdultContent: checked }))} - /> -
- - {/* Auto-play trailers */} -
-
- -

- Automatically play trailers when viewing media -

-
- setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))} - /> -
-
-
- - {/* Language Settings */} -
-

Language

-
-
- - -
-
- {LANGUAGE_OPTIONS.map((option) => ( - - ))} -
-
-
- - {/* Page Settings */} -
-

Page Settings

-
- {/* Page Title */} -
-
- - -
- 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" - /> -

- Custom title for your page. Leave empty to use the default title. -

-
- - {/* Favicon Upload */} -
-
- - -
-
- {faviconPreview && ( -
- Favicon preview - -
- )} -
- - -
-
-

- Upload a custom favicon or icon. The image will be converted to Base64 format. -

-
- - {/* Custom Colors */} -
-
- - -
-
- {[ - { 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 }) => ( -
- handleColorChange(key as keyof CustomColors, e.target.value)} - className="w-10 h-10 rounded-lg cursor-pointer border-0" - /> -
- - 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" +
+
+ +
+
+

{category}

+

+ {isEnabled ? 'Visible in library' : 'Hidden'} +

+
+
+ toggleCategory(category)} />
-
- ))} + ); + })}
-

- Leave color fields empty to use the default theme colors. -

-
+ + + + + {/* Display Settings */} + +
+ + + View Options + Configure how items are displayed + + + {/* Items per page */} +
+ +
+ {ITEMS_PER_PAGE_OPTIONS.map((option) => ( + + ))} +
+
+ + + + {/* Default view */} +
+ +
+ + +
+
+ + + + {/* Grid item size */} +
+
+ + {settings.gridItemSize} +
+
+ Small + setSettings(prev => ({ ...prev, gridItemSize: value }))} + className="flex-1" + /> + Large +
+
+
+
+ + + + + + Language + + Interface language preference + + +
+ {LANGUAGE_OPTIONS.map((option) => ( + + ))} +
+
+
-
-
+ + + {/* Content Settings */} + + + + Content Preferences + Control what content is shown + + +
+
+ +

Display adult media in your library

+
+ setSettings(prev => ({ ...prev, showAdultContent: checked }))} + /> +
+ +
+
+ +

Automatically play trailers when viewing media

+
+ setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))} + /> +
+
+
+
+ + {/* Appearance Settings */} + +
+ + + + + Theme + + Choose your preferred color scheme + + +
+ {([ + { 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 }) => ( + + ))} +
+
+
+ + + + + + Page Title + + Customize the page title + + + setPageTitle(e.target.value)} + placeholder="Leave empty for default title" + /> +

+ Custom title for your page. Leave empty to use the default title. +

+
+
+ + + + + + Favicon + + Upload a custom favicon + + +
+ {faviconPreview && ( +
+ Favicon preview + +
+ )} +
+ + +
+
+

+ The image will be converted to Base64 format. +

+
+
+ + + + + + Custom Colors + + Customize the application colors + + +
+ {[ + { 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 }) => ( +
+ +
+ handleColorChange(key as keyof CustomColors, e.target.value)} + className="w-10 h-10 rounded-lg cursor-pointer border-0 p-0" + /> + handleColorChange(key as keyof CustomColors, e.target.value)} + placeholder="#6d28d9" + className="flex-1 text-xs" + /> +
+
+ ))} +
+

+ Leave color fields empty to use the default theme colors. +

+
+
+
+
+
); diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx new file mode 100644 index 0000000..10b7591 --- /dev/null +++ b/src/components/ui/slider.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +interface SliderProps extends React.InputHTMLAttributes { + value?: number + min?: number + max?: number + step?: number + onValueChange?: (value: number) => void +} + +const Slider = React.forwardRef( + ({ className, value, min = 0, max = 100, step = 1, onValueChange, onChange, ...props }, ref) => { + const handleChange = (e: React.ChangeEvent) => { + const newValue = Number(e.target.value) + onValueChange?.(newValue) + onChange?.(e) + } + + return ( + + ) + } +) +Slider.displayName = "Slider" + +export { Slider }