From ba947cea87e285431f97b5aac2f312fdb62a2af9 Mon Sep 17 00:00:00 2001 From: caitlin Date: Mon, 22 Jun 2026 16:26:45 +0200 Subject: [PATCH] I fixed the lightmode on the sidebar and added more colour themes. I fixed the settings buttons. --- .gitignore | 4 + database/migrations/009_settings.sql | 22 ++ database/migrations/run_all.sql | 3 + src/app/api/settings/company/route.ts | 62 ++++ src/app/api/settings/preferences/route.ts | 60 ++++ src/app/globals.css | 266 +++++++++++++++++- .../settings/company-settings-form.tsx | 75 ++++- src/components/settings/theme-settings.tsx | 140 ++++++--- .../settings/user-preferences-form.tsx | 62 +++- src/providers/theme-provider.tsx | 9 +- 10 files changed, 652 insertions(+), 51 deletions(-) create mode 100644 database/migrations/009_settings.sql create mode 100644 src/app/api/settings/company/route.ts create mode 100644 src/app/api/settings/preferences/route.ts diff --git a/.gitignore b/.gitignore index c506f81..c1b3cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.git +.git_bak +node_modules +target \ No newline at end of file diff --git a/database/migrations/009_settings.sql b/database/migrations/009_settings.sql new file mode 100644 index 0000000..5874ba0 --- /dev/null +++ b/database/migrations/009_settings.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS company_settings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + company_name VARCHAR(255) NOT NULL DEFAULT '', + company_email VARCHAR(255) NOT NULL DEFAULT '', + company_phone VARCHAR(50) NOT NULL DEFAULT '', + company_website VARCHAR(255) NOT NULL DEFAULT '', + company_address TEXT NOT NULL DEFAULT '', + updated_by UUID REFERENCES users(id), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +INSERT INTO company_settings (company_name, company_email, company_phone, company_website, company_address) +VALUES ('Coastal IT Solutions', 'info@coastalit.com', '(555) 123-4567', 'https://coastalit.com', '123 Business Ave, Suite 100, San Francisco, CA 94105') +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + timezone VARCHAR(100) NOT NULL DEFAULT 'america-los_angeles', + date_format VARCHAR(10) NOT NULL DEFAULT 'mdy', + items_per_page INTEGER NOT NULL DEFAULT 20, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/database/migrations/run_all.sql b/database/migrations/run_all.sql index c5d75d0..b1add20 100644 --- a/database/migrations/run_all.sql +++ b/database/migrations/run_all.sql @@ -28,6 +28,9 @@ \echo '=== Running 008_notifications.sql (Notifications + Preferences) ===' \i 008_notifications.sql +\echo '=== Running 009_settings.sql (Company Settings + User Preferences) ===' +\i 009_settings.sql + \echo '=== Migration Complete ===' \echo '' \echo 'Test accounts created:' diff --git a/src/app/api/settings/company/route.ts b/src/app/api/settings/company/route.ts new file mode 100644 index 0000000..7499a4f --- /dev/null +++ b/src/app/api/settings/company/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server" +import { getSessionUser } from "@/lib/auth" +import { query } from "@/lib/db" + +export async function GET() { + try { + const user = await getSessionUser() + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + + const result = await query(`SELECT * FROM company_settings ORDER BY updated_at DESC LIMIT 1`) + const row = result.rows[0] + + if (!row) { + return NextResponse.json({ + companyName: "Coastal IT Solutions", + companyEmail: "info@coastalit.com", + companyPhone: "(555) 123-4567", + companyWebsite: "https://coastalit.com", + companyAddress: "123 Business Ave, Suite 100, San Francisco, CA 94105", + }) + } + + return NextResponse.json({ + companyName: row.company_name || "", + companyEmail: row.company_email || "", + companyPhone: row.company_phone || "", + companyWebsite: row.company_website || "", + companyAddress: row.company_address || "", + }) + } catch (error) { + console.error("Company settings GET error:", error) + return NextResponse.json({ error: "Failed to load company settings" }, { status: 500 }) + } +} + +export async function PATCH(request: NextRequest) { + try { + const user = await getSessionUser() + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + + const body = await request.json() + + await query( + `UPDATE company_settings SET + company_name = $1, company_email = $2, company_phone = $3, + company_website = $4, company_address = $5, updated_by = $6, updated_at = NOW()`, + [ + body.companyName || "", + body.companyEmail || "", + body.companyPhone || "", + body.companyWebsite || "", + body.companyAddress || "", + user.id, + ], + ) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Company settings PATCH error:", error) + return NextResponse.json({ error: "Failed to save company settings" }, { status: 500 }) + } +} diff --git a/src/app/api/settings/preferences/route.ts b/src/app/api/settings/preferences/route.ts new file mode 100644 index 0000000..6a7da54 --- /dev/null +++ b/src/app/api/settings/preferences/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from "next/server" +import { getSessionUser } from "@/lib/auth" +import { query } from "@/lib/db" + +export async function GET() { + try { + const user = await getSessionUser() + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + + const result = await query( + `SELECT timezone, date_format, items_per_page FROM user_preferences WHERE user_id = $1`, + [user.id], + ) + + if (result.rowCount === 0) { + return NextResponse.json({ + timezone: "america-los_angeles", + dateFormat: "mdy", + itemsPerPage: 20, + }) + } + + const r = result.rows[0] + return NextResponse.json({ + timezone: r.timezone, + dateFormat: r.date_format, + itemsPerPage: r.items_per_page, + }) + } catch (error) { + console.error("Preferences GET error:", error) + return NextResponse.json({ error: "Failed to load preferences" }, { status: 500 }) + } +} + +export async function PATCH(request: NextRequest) { + try { + const user = await getSessionUser() + if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + + const body = await request.json() + + await query( + `INSERT INTO user_preferences (user_id, timezone, date_format, items_per_page, updated_at) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (user_id) + DO UPDATE SET timezone = $2, date_format = $3, items_per_page = $4, updated_at = NOW()`, + [ + user.id, + body.timezone || "america-los_angeles", + body.dateFormat || "mdy", + body.itemsPerPage || 20, + ], + ) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Preferences PATCH error:", error) + return NextResponse.json({ error: "Failed to save preferences" }, { status: 500 }) + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 5489282..b50acdf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -22,14 +22,14 @@ --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 221.2 83.2% 53.3%; - --sidebar: 222.2 84% 4.9%; - --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 221.2 83.2% 53.3%; --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; - --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 224.3 76.3% 48%; + --sidebar-accent: 210 40% 96.1%; + --sidebar-accent-foreground: 222.2 84% 4.9%; + --sidebar-border: 214.3 31.8% 91.4%; + --sidebar-ring: 221.2 83.2% 53.3%; --radius: 0.5rem; } @@ -63,6 +63,258 @@ --sidebar-ring: 224.3 76.3% 48%; } +.ocean { + --primary: 187 75% 42%; + --primary-foreground: 210 40% 98%; + --ring: 187 75% 42%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 187 75% 42%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 187 30% 94%; + --sidebar-accent-foreground: 187 50% 20%; + --sidebar-border: 187 20% 88%; + --sidebar-ring: 187 75% 42%; +} + +.dark.ocean { + --primary: 187 75% 50%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 187 75% 50%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 187 75% 55%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 187 75% 50%; +} + +.forest { + --primary: 142 76% 36%; + --primary-foreground: 210 40% 98%; + --ring: 142 76% 36%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 142 76% 36%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 142 30% 94%; + --sidebar-accent-foreground: 142 50% 20%; + --sidebar-border: 142 20% 88%; + --sidebar-ring: 142 76% 36%; +} + +.dark.forest { + --primary: 142 76% 44%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 142 76% 44%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 142 76% 50%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 142 76% 44%; +} + +.sunset { + --primary: 24 95% 53%; + --primary-foreground: 210 40% 98%; + --ring: 24 95% 53%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 24 95% 53%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 24 30% 94%; + --sidebar-accent-foreground: 24 50% 20%; + --sidebar-border: 24 20% 88%; + --sidebar-ring: 24 95% 53%; +} + +.dark.sunset { + --primary: 24 95% 58%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 24 95% 58%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 24 95% 65%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 24 95% 58%; +} + +.midnight { + --primary: 230 75% 55%; + --primary-foreground: 210 40% 98%; + --ring: 230 75% 55%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 230 75% 55%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 230 30% 94%; + --sidebar-accent-foreground: 230 50% 20%; + --sidebar-border: 230 20% 88%; + --sidebar-ring: 230 75% 55%; +} + +.dark.midnight { + --primary: 230 75% 62%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 230 75% 62%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 230 75% 65%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 230 75% 62%; +} + +.rose { + --primary: 346 77% 50%; + --primary-foreground: 210 40% 98%; + --ring: 346 77% 50%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 346 77% 50%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 346 30% 94%; + --sidebar-accent-foreground: 346 50% 20%; + --sidebar-border: 346 20% 88%; + --sidebar-ring: 346 77% 50%; +} + +.dark.rose { + --primary: 346 77% 58%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 346 77% 58%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 346 77% 60%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 346 77% 58%; +} + +.amber { + --primary: 38 92% 50%; + --primary-foreground: 210 40% 98%; + --ring: 38 92% 50%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 38 92% 50%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 38 30% 94%; + --sidebar-accent-foreground: 38 50% 20%; + --sidebar-border: 38 20% 88%; + --sidebar-ring: 38 92% 50%; +} + +.dark.amber { + --primary: 38 92% 56%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 38 92% 56%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 38 92% 60%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 38 92% 56%; +} + +.violet { + --primary: 262 83% 58%; + --primary-foreground: 210 40% 98%; + --ring: 262 83% 58%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 262 83% 58%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 262 30% 94%; + --sidebar-accent-foreground: 262 50% 20%; + --sidebar-border: 262 20% 88%; + --sidebar-ring: 262 83% 58%; +} + +.dark.violet { + --primary: 262 83% 65%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 262 83% 65%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 262 83% 68%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 262 83% 65%; +} + +.slate { + --primary: 215 20% 45%; + --primary-foreground: 210 40% 98%; + --ring: 215 20% 45%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 215 20% 45%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 215 20% 94%; + --sidebar-accent-foreground: 215 50% 20%; + --sidebar-border: 215 20% 88%; + --sidebar-ring: 215 20% 45%; +} + +.dark.slate { + --primary: 215 20% 60%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 215 20% 60%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 215 20% 65%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 215 20% 60%; +} + +.ruby { + --primary: 351 85% 45%; + --primary-foreground: 210 40% 98%; + --ring: 351 85% 45%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 351 85% 45%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 351 30% 94%; + --sidebar-accent-foreground: 351 50% 20%; + --sidebar-border: 351 20% 88%; + --sidebar-ring: 351 85% 45%; +} + +.dark.ruby { + --primary: 351 85% 52%; + --primary-foreground: 222.2 47.4% 11.2%; + --ring: 351 85% 52%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 351 85% 55%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 351 85% 52%; +} + @layer base { * { @apply border-border; diff --git a/src/components/settings/company-settings-form.tsx b/src/components/settings/company-settings-form.tsx index bc20de3..38dbaa1 100644 --- a/src/components/settings/company-settings-form.tsx +++ b/src/components/settings/company-settings-form.tsx @@ -1,13 +1,72 @@ "use client" +import { useEffect, useState } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" -import { COMPANY_NAME } from "@/lib/constants" import { toast } from "sonner" +interface CompanyData { + companyName: string + companyEmail: string + companyPhone: string + companyWebsite: string + companyAddress: string +} + export function CompanySettingsForm() { + const [data, setData] = useState({ + companyName: "", + companyEmail: "", + companyPhone: "", + companyWebsite: "", + companyAddress: "", + }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + async function load() { + try { + const res = await fetch("/api/settings/company") + if (res.ok) { + const json = await res.json() + setData(json) + } + } catch { + // ignore + } finally { + setLoading(false) + } + } + load() + }, []) + + async function handleSave() { + setSaving(true) + try { + const res = await fetch("/api/settings/company", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }) + if (res.ok) { + toast.success("Company settings saved") + } else { + toast.error("Failed to save company settings") + } + } catch { + toast.error("Failed to save company settings") + } finally { + setSaving(false) + } + } + + function update(field: keyof CompanyData, value: string) { + setData((prev) => ({ ...prev, [field]: value })) + } + return ( @@ -20,27 +79,29 @@ export function CompanySettingsForm() {
- + update("companyName", e.target.value)} />
- + update("companyEmail", e.target.value)} />
- + update("companyPhone", e.target.value)} />
- + update("companyWebsite", e.target.value)} />
- + update("companyAddress", e.target.value)} />
- +
diff --git a/src/components/settings/theme-settings.tsx b/src/components/settings/theme-settings.tsx index c70d9f4..3879277 100644 --- a/src/components/settings/theme-settings.tsx +++ b/src/components/settings/theme-settings.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import { useTheme } from "next-themes" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" @@ -7,42 +8,117 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { cn } from "@/lib/utils" import { Sun, Moon, Monitor } from "lucide-react" +const COLOR_THEME_KEY = "crm-color-theme" + +const modeOptions = [ + { value: "light", icon: Sun, label: "Light" }, + { value: "dark", icon: Moon, label: "Dark" }, + { value: "system", icon: Monitor, label: "System" }, +] + +const themeOptions = [ + { value: "default", label: "Default", color: "bg-blue-600", ring: "ring-blue-600" }, + { value: "ocean", label: "Ocean", color: "bg-teal-500", ring: "ring-teal-500" }, + { value: "forest", label: "Forest", color: "bg-green-600", ring: "ring-green-600" }, + { value: "sunset", label: "Sunset", color: "bg-orange-500", ring: "ring-orange-500" }, + { value: "midnight", label: "Midnight", color: "bg-indigo-600", ring: "ring-indigo-600" }, + { value: "rose", label: "Rose", color: "bg-pink-600", ring: "ring-pink-600" }, + { value: "amber", label: "Amber", color: "bg-amber-500", ring: "ring-amber-500" }, + { value: "violet", label: "Violet", color: "bg-violet-600", ring: "ring-violet-600" }, + { value: "slate", label: "Slate", color: "bg-slate-500", ring: "ring-slate-500" }, + { value: "ruby", label: "Ruby", color: "bg-red-600", ring: "ring-red-600" }, +] + +function getStoredColorTheme(): string { + if (typeof window === "undefined") return "default" + return localStorage.getItem(COLOR_THEME_KEY) || "default" +} + +function applyColorTheme(theme: string) { + document.documentElement.className = document.documentElement.className + .replace(/\b(default|ocean|forest|sunset|midnight|rose|amber|violet|slate|ruby)\b/g, "") + .trim() + if (theme !== "default") { + document.documentElement.classList.add(theme) + } + localStorage.setItem(COLOR_THEME_KEY, theme) +} + export function ThemeSettings() { const { theme, setTheme } = useTheme() + const [colorTheme, setColorTheme] = useState("default") + const [mounted, setMounted] = useState(false) - const options = [ - { value: "light", icon: Sun, label: "Light" }, - { value: "dark", icon: Moon, label: "Dark" }, - { value: "system", icon: Monitor, label: "System" }, - ] + useEffect(() => { + setMounted(true) + setColorTheme(getStoredColorTheme()) + applyColorTheme(getStoredColorTheme()) + }, []) + + function handleColorChange(value: string) { + setColorTheme(value) + applyColorTheme(value) + } + + if (!mounted) return null return ( - - - Theme Settings - - Choose your preferred appearance for the dashboard. - - - - - {options.map(({ value, icon: Icon, label }) => ( -
- - -
- ))} -
-
-
+
+ + + Theme Mode + + Choose your preferred appearance mode. + + + + + {modeOptions.map(({ value, icon: Icon, label }) => ( +
+ + +
+ ))} +
+
+
+ + + + Color Theme + + Select a color accent for the dashboard. + + + + + {themeOptions.map(({ value, label, color, ring }) => ( +
+ +
) } diff --git a/src/components/settings/user-preferences-form.tsx b/src/components/settings/user-preferences-form.tsx index 68d2481..3e419aa 100644 --- a/src/components/settings/user-preferences-form.tsx +++ b/src/components/settings/user-preferences-form.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" @@ -12,7 +13,58 @@ import { SelectValue, } from "@/components/ui/select" +interface Preferences { + timezone: string + dateFormat: string + itemsPerPage: number +} + export function UserPreferencesForm() { + const [prefs, setPrefs] = useState({ + timezone: "america-los_angeles", + dateFormat: "mdy", + itemsPerPage: 20, + }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + async function load() { + try { + const res = await fetch("/api/settings/preferences") + if (res.ok) { + const json = await res.json() + setPrefs(json) + } + } catch { + // ignore + } finally { + setLoading(false) + } + } + load() + }, []) + + async function handleSave() { + setSaving(true) + try { + const res = await fetch("/api/settings/preferences", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(prefs), + }) + if (res.ok) { + toast.success("Preferences saved") + } else { + toast.error("Failed to save preferences") + } + } catch { + toast.error("Failed to save preferences") + } finally { + setSaving(false) + } + } + return ( @@ -25,7 +77,7 @@ export function UserPreferencesForm() {
- setPrefs((p) => ({ ...p, timezone: v }))}> @@ -39,7 +91,7 @@ export function UserPreferencesForm() {
- setPrefs((p) => ({ ...p, dateFormat: v }))}> @@ -52,7 +104,7 @@ export function UserPreferencesForm() {
- setPrefs((p) => ({ ...p, itemsPerPage: parseInt(v) }))}> @@ -66,7 +118,9 @@ export function UserPreferencesForm() {
- +
diff --git a/src/providers/theme-provider.tsx b/src/providers/theme-provider.tsx index b912564..027eb3b 100644 --- a/src/providers/theme-provider.tsx +++ b/src/providers/theme-provider.tsx @@ -4,5 +4,12 @@ import { ThemeProvider as NextThemesProvider } from "next-themes" import { type ThemeProviderProps } from "next-themes" export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} + return ( + + {children} + + ) }