From 5b5503b7a6d8a4cc42224d3c09c68dfda0fd8f2a Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:15:08 -0400 Subject: [PATCH] add user settings --- src/app/api/settings/route.ts | 39 ++++++ src/app/settings/SettingsForm.tsx | 136 +++++++++++++++++++++ src/app/settings/page.tsx | 27 ++++ src/components/HeaderNav.tsx | 11 +- src/components/mixed/VideoPlayerModal.tsx | 20 +-- src/components/movies/MovieDetailModal.tsx | 1 + src/components/tv/TvView.tsx | 1 + src/hooks/useUserSettings.ts | 31 +++++ src/lib/db.ts | 13 ++ src/lib/settings.ts | 81 ++++++++++++ src/types/index.ts | 12 ++ 11 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 src/app/api/settings/route.ts create mode 100644 src/app/settings/SettingsForm.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/hooks/useUserSettings.ts create mode 100644 src/lib/settings.ts diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts new file mode 100644 index 0000000..ad18e5b --- /dev/null +++ b/src/app/api/settings/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import { getUserSettings, updateUserSettings } from '@/lib/settings' +import type { UserSettings } from '@/types' + +export async function GET(req: NextRequest) { + const auth = await requireAuth(req) + if (auth instanceof NextResponse) return auth + + const settings = getUserSettings(auth.session.userId) + return NextResponse.json(settings) +} + +export async function PUT(req: NextRequest) { + const auth = await requireAuth(req) + if (auth instanceof NextResponse) return auth + + let body: unknown + try { + body = await req.json() + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + + const s = body as Record + const boolFields: (keyof UserSettings)[] = [ + 'mixedAutoplay', 'mixedLoop', 'mixedMuted', + 'moviesAutoplay', 'moviesLoop', 'moviesMuted', + 'tvAutoplay', 'tvLoop', 'tvMuted', + ] + for (const field of boolFields) { + if (typeof s[field] !== 'boolean') { + return NextResponse.json({ error: `Invalid value for ${field}` }, { status: 400 }) + } + } + + updateUserSettings(auth.session.userId, s as unknown as UserSettings) + return NextResponse.json({ ok: true }) +} diff --git a/src/app/settings/SettingsForm.tsx b/src/app/settings/SettingsForm.tsx new file mode 100644 index 0000000..96a25c4 --- /dev/null +++ b/src/app/settings/SettingsForm.tsx @@ -0,0 +1,136 @@ +'use client' + +import { useState } from 'react' +import type { UserSettings } from '@/types' + +interface Props { + initialSettings: UserSettings +} + +interface ToggleProps { + label: string + checked: boolean + onChange: (v: boolean) => void +} + +function Toggle({ label, checked, onChange }: ToggleProps) { + return ( + + ) +} + +interface SectionProps { + title: string + icon: string + children: React.ReactNode +} + +function SettingsSection({ title, icon, children }: SectionProps) { + return ( +
+

+ {icon} + {title} +

+
+ {children} +
+
+ ) +} + +export default function SettingsForm({ initialSettings }: Props) { + const [settings, setSettings] = useState(initialSettings) + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + const [error, setError] = useState(null) + + function set(key: K, value: boolean) { + setSettings((prev) => ({ ...prev, [key]: value })) + setSaved(false) + } + + async function handleSave() { + setSaving(true) + setError(null) + try { + const res = await fetch('/api/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }) + if (!res.ok) throw new Error('Failed to save') + setSaved(true) + } catch { + setError('Failed to save settings. Please try again.') + } finally { + setSaving(false) + } + } + + return ( +
+ + set('mixedAutoplay', v)} /> + set('mixedLoop', v)} /> + set('mixedMuted', v)} /> + + + + set('moviesAutoplay', v)} /> + set('moviesLoop', v)} /> + set('moviesMuted', v)} /> + + + + set('tvAutoplay', v)} /> + set('tvLoop', v)} /> + set('tvMuted', v)} /> + + +
+ + {saved && ( + + Saved + + )} + {error && ( + + {error} + + )} +
+
+ ) +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..5fbede2 --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,27 @@ +import { redirect } from 'next/navigation' +import { getServerSession } from '@/lib/auth' +import { getUserSettings } from '@/lib/settings' +import SettingsForm from './SettingsForm' + +export default async function SettingsPage() { + const session = await getServerSession() + if (!session.userId) redirect('/login') + + const settings = getUserSettings(session.userId) + + return ( +
+

+ Settings +

+

+ Signed in as {session.username} +

+ +

+ Video Playback +

+ +
+ ) +} diff --git a/src/components/HeaderNav.tsx b/src/components/HeaderNav.tsx index f9fafd0..80f4e76 100644 --- a/src/components/HeaderNav.tsx +++ b/src/components/HeaderNav.tsx @@ -1,6 +1,7 @@ 'use client' import { useRouter } from 'next/navigation' +import Link from 'next/link' import NavLink from './NavLink' interface Props { @@ -20,9 +21,15 @@ export default function HeaderNav({ username, isAdmin }: Props) { return (
{isAdmin && Manage} - + ((e.currentTarget as HTMLElement).style.color = 'var(--text-primary)')} + onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)')} + > {username} - +