Add user settings UI and API integration

Introduce a full user settings feature: add a SettingsView component and UserSettings type, plus API helpers to fetch, create, and update settings (convertors between API and app shapes). App now loads settings on mount, persists category toggles to the API, exposes a /settings route, and passes itemsPerPage into BrowseView and CastView. Header gains a settings icon/link and BrowseView/CastView update pagination option defaults. This enables centralized library/display/content preferences and syncs them with the backend.
This commit is contained in:
Lars Behrends
2026-04-10 14:14:27 +02:00
parent f5c3e96823
commit 04156486e2
7 changed files with 555 additions and 16 deletions

View File

@@ -13,9 +13,10 @@ import CastView from './components/CastView';
import CastDetailView from './components/CastDetailView'; import CastDetailView from './components/CastDetailView';
import AddMediaView from './components/AddMediaView'; import AddMediaView from './components/AddMediaView';
import ImporterView from './components/ImporterView'; import ImporterView from './components/ImporterView';
import SettingsView from './components/SettingsView';
import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data';
import { Media, Staff, MediaCategory } from './types'; import { Media, Staff, MediaCategory, UserSettings } from './types';
import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff } from './api'; import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api';
function AppContent() { function AppContent() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -28,12 +29,41 @@ function AppContent() {
const [selectedPerson, setSelectedPerson] = useState<Staff | null>(null); const [selectedPerson, setSelectedPerson] = useState<Staff | null>(null);
const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || ''); const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || '');
const [enabledCategories, setEnabledCategories] = useState<MediaCategory[]>(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); const [enabledCategories, setEnabledCategories] = useState<MediaCategory[]>(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']);
const [settings, setSettings] = useState<UserSettings | null>(null);
const [customMedia, setCustomMedia] = useState<Media[]>([]); const [customMedia, setCustomMedia] = useState<Media[]>([]);
const [adultMedia, setAdultMedia] = useState<Media[]>([]); const [adultMedia, setAdultMedia] = useState<Media[]>([]);
// Load media from API on component mount (only when not on cast routes) // Load media from API on component mount (only when not on cast routes)
const [apiMedia, setApiMedia] = useState<Media[]>([]); const [apiMedia, setApiMedia] = useState<Media[]>([]);
useEffect(() => {
const loadSettingsFromApi = async () => {
try {
const loadedSettings = await fetchSettings();
if (loadedSettings) {
setSettings(loadedSettings);
setEnabledCategories(loadedSettings.enabledCategories);
}
} catch (error) {
console.error('Failed to load settings from API:', error);
}
};
loadSettingsFromApi();
}, []);
const reloadSettings = async () => {
try {
const loadedSettings = await fetchSettings();
if (loadedSettings) {
setSettings(loadedSettings);
setEnabledCategories(loadedSettings.enabledCategories);
}
} catch (error) {
console.error('Failed to reload settings from API:', error);
}
};
useEffect(() => { useEffect(() => {
const loadMediaFromApi = async () => { const loadMediaFromApi = async () => {
try { try {
@@ -50,7 +80,7 @@ function AppContent() {
} }
}, [location.pathname]); }, [location.pathname]);
const toggleCategory = (category: MediaCategory) => { const toggleCategory = async (category: MediaCategory) => {
setEnabledCategories(prev => { setEnabledCategories(prev => {
const isEnabling = !prev.includes(category); const isEnabling = !prev.includes(category);
const newList = isEnabling const newList = isEnabling
@@ -62,6 +92,27 @@ function AppContent() {
const nextCategory = newList.find(c => c !== category) || 'Anime'; const nextCategory = newList.find(c => c !== category) || 'Anime';
setActiveCategory(nextCategory as MediaCategory); setActiveCategory(nextCategory as MediaCategory);
} }
// Save to API
const baseSettings = settings || {
enabledCategories: prev,
itemsPerPage: 20,
defaultView: 'grid',
showAdultContent: false,
autoPlayTrailers: false,
language: 'en',
theme: 'system',
};
const updatedSettings: UserSettings = {
...baseSettings,
enabledCategories: newList,
};
updateSettings(updatedSettings).then(saved => {
if (saved) {
setSettings(saved);
}
});
return newList; return newList;
}); });
}; };
@@ -237,6 +288,7 @@ function AppContent() {
mediaList={filteredMedia} mediaList={filteredMedia}
onMediaClick={handleMediaClick} onMediaClick={handleMediaClick}
activeCategory={activeCategory} activeCategory={activeCategory}
itemsPerPage={settings?.itemsPerPage}
/> />
} /> } />
<Route path="/media/:id" element={ <Route path="/media/:id" element={
@@ -251,6 +303,7 @@ function AppContent() {
<CastView <CastView
onPersonClick={handlePersonClick} onPersonClick={handlePersonClick}
enabledCategories={enabledCategories} enabledCategories={enabledCategories}
itemsPerPage={settings?.itemsPerPage}
/> />
} /> } />
<Route path="/cast/:id" element={ <Route path="/cast/:id" element={
@@ -268,6 +321,9 @@ function AppContent() {
<Route path="/import" element={ <Route path="/import" element={
<ImporterView /> <ImporterView />
} /> } />
<Route path="/settings" element={
<SettingsView onSettingsSaved={reloadSettings} />
} />
</Routes> </Routes>
</LayoutGroup> </LayoutGroup>
</main> </main>

View File

@@ -1,4 +1,4 @@
import { Media, Staff } from './types'; import { Media, Staff, UserSettings, MediaCategory } from './types';
const BASE_URL = 'http://192.168.1.102:6400'; const BASE_URL = 'http://192.168.1.102:6400';
@@ -625,3 +625,144 @@ export async function fetchMediaFromApi(apiUrl?: string): Promise<Media[]> {
export async function fetchMediaFromLocalJson(): Promise<Media[]> { export async function fetchMediaFromLocalJson(): Promise<Media[]> {
return fetchAllMedia(); return fetchAllMedia();
} }
// Settings API Types
export interface ApiSettingsItem {
id?: number;
enabled_categories: string[];
items_per_page: number;
default_view: string;
show_adult_content: boolean;
auto_play_trailers: boolean;
language: string;
theme: string;
created_at?: string;
updated_at?: string;
}
export interface CreateSettingsInput {
enabled_categories: string[];
items_per_page?: number;
default_view?: string;
show_adult_content?: boolean;
auto_play_trailers?: boolean;
language?: string;
theme?: string;
}
export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {}
export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings {
return {
id: apiItem.id,
enabledCategories: apiItem.enabled_categories as MediaCategory[],
itemsPerPage: apiItem.items_per_page || 20,
defaultView: (apiItem.default_view as 'grid' | 'list') || 'grid',
showAdultContent: apiItem.show_adult_content || false,
autoPlayTrailers: apiItem.auto_play_trailers || false,
language: apiItem.language || 'en',
theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system',
createdAt: apiItem.created_at,
updatedAt: apiItem.updated_at,
};
}
export function convertSettingsToApi(settings: UserSettings): CreateSettingsInput {
return {
enabled_categories: settings.enabledCategories,
items_per_page: settings.itemsPerPage,
default_view: settings.defaultView,
show_adult_content: settings.showAdultContent,
auto_play_trailers: settings.autoPlayTrailers,
language: settings.language,
theme: settings.theme,
};
}
// Settings API Functions
export async function fetchSettings(): Promise<UserSettings | null> {
try {
const response = await fetch(`${BASE_URL}/api/settings`);
if (!response.ok) {
// If settings don't exist (404), return null to use defaults
if (response.status === 404) {
return null;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse<ApiSettingsItem> = await response.json();
if (data.success && data.data) {
return convertApiToSettings(data.data);
}
return null;
} catch (error) {
console.error('Error fetching settings:', error);
return null;
}
}
export async function createSettings(settings: UserSettings): Promise<UserSettings | null> {
try {
const apiSettings = convertSettingsToApi(settings);
console.log('Creating settings:', apiSettings);
const response = await fetch(`${BASE_URL}/api/settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(apiSettings),
});
console.log('Create settings response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('Create settings error response:', errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse<ApiSettingsItem> = await response.json();
console.log('Create settings response:', data);
if (data.success && data.data) {
return convertApiToSettings(data.data);
}
return null;
} catch (error) {
console.error('Error creating settings:', error);
return null;
}
}
export async function updateSettings(settings: UserSettings): Promise<UserSettings | null> {
try {
const apiSettings = convertSettingsToApi(settings);
console.log('Updating settings:', apiSettings);
const response = await fetch(`${BASE_URL}/api/settings`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(apiSettings),
});
console.log('Update settings response status:', response.status);
if (!response.ok) {
// If settings don't exist (404), try creating them instead
if (response.status === 404) {
console.log('Settings not found, attempting to create...');
return createSettings(settings);
}
const errorText = await response.text();
console.error('Update settings error response:', errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiResponse<ApiSettingsItem> = await response.json();
console.log('Update settings response:', data);
if (data.success && data.data) {
return convertApiToSettings(data.data);
}
return null;
} catch (error) {
console.error('Error updating settings:', error);
return null;
}
}

View File

@@ -17,12 +17,13 @@ interface BrowseViewProps {
mediaList: Media[]; mediaList: Media[];
onMediaClick: (media: Media) => void; onMediaClick: (media: Media) => void;
activeCategory: MediaCategory; activeCategory: MediaCategory;
itemsPerPage?: number;
} }
export default function BrowseView({ mediaList, onMediaClick, activeCategory }: BrowseViewProps) { export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12 }: BrowseViewProps) {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(12); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
const [sortBy, setSortBy] = useState<string>('default'); const [sortBy, setSortBy] = useState<string>('default');
// Filter states // Filter states
@@ -281,7 +282,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory }:
}} }}
className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{[8, 12, 16, 24, 48].map(size => ( {[12, 20, 36, 48, 60].map(size => (
<option key={size} value={size}>{size}</option> <option key={size} value={size}>{size}</option>
))} ))}
</select> </select>

View File

@@ -12,9 +12,10 @@ import { fetchAllCast } from '@/api';
interface CastViewProps { interface CastViewProps {
onPersonClick: (person: Staff) => void; onPersonClick: (person: Staff) => void;
enabledCategories: MediaCategory[]; enabledCategories: MediaCategory[];
itemsPerPage?: number;
} }
export default function CastView({ onPersonClick, enabledCategories }: CastViewProps) { export default function CastView({ onPersonClick, enabledCategories, itemsPerPage: initialItemsPerPage = 12 }: CastViewProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [staffList, setStaffList] = useState<Staff[]>([]); const [staffList, setStaffList] = useState<Staff[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -34,7 +35,7 @@ export default function CastView({ onPersonClick, enabledCategories }: CastViewP
return localStorage.getItem('castFilterMediaType') || ''; return localStorage.getItem('castFilterMediaType') || '';
}); });
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(12); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
// Persist filters and sorts // Persist filters and sorts
@@ -382,7 +383,7 @@ export default function CastView({ onPersonClick, enabledCategories }: CastViewP
}} }}
className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none"
> >
{[8, 12, 16, 24, 48].map(size => ( {[12, 20, 36, 48, 60].map(size => (
<option key={size} value={size}>{size}</option> <option key={size} value={size}>{size}</option>
))} ))}
</select> </select>

View File

@@ -1,9 +1,8 @@
import { Search, User, X, Plus, Download } from 'lucide-react'; import { Search, User, X, Plus, Download, Settings } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { MediaCategory } from '@/types'; import { MediaCategory } from '@/types';
import LibrarySettings from './LibrarySettings';
interface HeaderProps { interface HeaderProps {
onSearch: (query: string) => void; onSearch: (query: string) => void;
@@ -113,10 +112,12 @@ export default function Header({
> >
<Download size={20} /> <Download size={20} />
</Link> </Link>
<LibrarySettings <Link
enabledCategories={enabledCategories} to="/settings"
onToggleCategory={onToggleCategory} className="p-2 text-white/90 hover:text-white transition-colors"
/> >
<Settings size={20} />
</Link>
<button className="w-8 h-8 rounded-full overflow-hidden border-2 border-white/20"> <button className="w-8 h-8 rounded-full overflow-hidden border-2 border-white/20">
<img <img
src="https://picsum.photos/seed/user/100/100" src="https://picsum.photos/seed/user/100/100"

View File

@@ -0,0 +1,326 @@
import React, { useState, useEffect } from 'react';
import { MediaCategory, UserSettings } 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 } from 'lucide-react';
import { Link } from 'react-router-dom';
import { fetchSettings, updateSettings } from '@/api';
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 ITEMS_PER_PAGE_OPTIONS = [12, 20, 36, 48, 60];
const LANGUAGE_OPTIONS = [
{ value: 'en', label: 'English' },
{ value: 'de', label: 'Deutsch' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'ja', label: '日本語' },
];
interface SettingsViewProps {
onSettingsSaved?: () => void;
}
export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const [settings, setSettings] = useState<UserSettings>({
enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'],
itemsPerPage: 20,
defaultView: 'grid',
showAdultContent: false,
autoPlayTrailers: false,
language: 'en',
theme: 'system',
});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
const loadedSettings = await fetchSettings();
if (loadedSettings) {
setSettings(loadedSettings);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
setIsSaving(true);
setSaveStatus('idle');
try {
const savedSettings = await updateSettings(settings);
if (savedSettings) {
setSettings(savedSettings);
setSaveStatus('success');
onSettingsSaved?.();
} else {
setSaveStatus('error');
}
} catch (error) {
console.error('Failed to save settings:', error);
setSaveStatus('error');
} finally {
setIsSaving(false);
setTimeout(() => setSaveStatus('idle'), 3000);
}
};
const toggleCategory = (category: MediaCategory) => {
setSettings(prev => ({
...prev,
enabledCategories: prev.enabledCategories.includes(category)
? prev.enabledCategories.filter(c => c !== category)
: [...prev.enabledCategories, category]
}));
};
if (isLoading) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="text-zinc-400 font-medium">Loading settings...</div>
</div>
);
}
return (
<div className="min-h-screen bg-white pt-20">
{/* Content */}
<div className="max-w-[1600px] 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-zinc-400 hover:text-[#6d28d9] transition-colors mb-2"
>
<ArrowLeft size={16} />
Back to home
</Link>
<h1 className="text-3xl font-black text-zinc-900">Settings</h1>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className="bg-[#6d28d9] text-white hover:bg-[#5b21b6] font-bold px-6 py-3 h-12 rounded-lg flex items-center gap-2 transition-colors disabled:opacity-50"
>
{isSaving ? (
'Saving...'
) : (
<>
<Save size={16} />
Save Changes
</>
)}
</button>
</div>
{saveStatus === 'success' && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl text-green-700 font-medium">
Settings saved successfully!
</div>
)}
{saveStatus === 'error' && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 font-medium">
Failed to save settings. Please try again.
</div>
)}
<div className="grid gap-8">
{/* Library Settings */}
<section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Library Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100">
<p className="text-sm font-medium text-zinc-500 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-white border border-zinc-100 transition-all hover:border-[#6d28d9]/20">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-zinc-50 flex items-center justify-center text-[#6d28d9]">
{CATEGORY_ICONS[category]}
</div>
<div>
<Label htmlFor={category} className="text-sm font-black text-zinc-900 cursor-pointer">
{category}
</Label>
<p className="text-[10px] font-bold text-zinc-400 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>
</div>
</section>
{/* Display Settings */}
<section>
<h2 className="text-xl font-black text-zinc-900 mb-6">Display Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100 space-y-6">
{/* Items per page */}
<div>
<Label className="text-sm font-black text-zinc-900 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-lg text-sm font-bold transition-all ${
settings.itemsPerPage === option
? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200'
}`}
>
{option}
</button>
))}
</div>
</div>
{/* Default view */}
<div>
<Label className="text-sm font-black text-zinc-900 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-lg text-sm font-bold transition-all ${
settings.defaultView === 'grid'
? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200'
}`}
>
<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-lg text-sm font-bold transition-all ${
settings.defaultView === 'list'
? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200'
}`}
>
<List size={18} />
List
</button>
</div>
</div>
{/* Theme */}
<div>
<Label className="text-sm font-black text-zinc-900 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-lg text-sm font-bold transition-all ${
settings.theme === theme
? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200'
}`}
>
{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-xl font-black text-zinc-900 mb-6">Content Settings</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100 space-y-4">
{/* Show adult content */}
<div className="flex items-center justify-between p-4 rounded-xl bg-white border border-zinc-100">
<div>
<Label htmlFor="showAdult" className="text-sm font-black text-zinc-900 cursor-pointer">
Show adult content
</Label>
<p className="text-xs font-medium text-zinc-500 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-white border border-zinc-100">
<div>
<Label htmlFor="autoPlay" className="text-sm font-black text-zinc-900 cursor-pointer">
Auto-play trailers
</Label>
<p className="text-xs font-medium text-zinc-500 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-xl font-black text-zinc-900 mb-6">Language</h2>
<div className="bg-zinc-50 rounded-2xl p-6 border border-zinc-100">
<div className="flex items-center gap-2 mb-4">
<Globe size={18} className="text-[#6d28d9]" />
<Label className="text-sm font-black text-zinc-900">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-lg text-sm font-bold transition-all ${
settings.language === option.value
? 'bg-[#6d28d9] text-white'
: 'bg-white text-zinc-600 hover:bg-zinc-100 border border-zinc-200'
}`}
>
{option.label}
</button>
))}
</div>
</div>
</section>
</div>
</div>
</div>
);
}

View File

@@ -95,3 +95,16 @@ export interface AdultSpecifics {
measurements?: string | null; measurements?: string | null;
shoe_size?: number | null; shoe_size?: number | null;
} }
export interface UserSettings {
id?: number;
enabledCategories: MediaCategory[];
itemsPerPage: number;
defaultView: 'grid' | 'list';
showAdultContent: boolean;
autoPlayTrailers: boolean;
language: string;
theme: 'light' | 'dark' | 'system';
createdAt?: string;
updatedAt?: string;
}