import { getCurrentSession } from "@/lib/session"; import { getActiveShareLinks } from "@/modules/_core/share"; import { getEntityType } from "@/modules/_core/registry"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ThemePicker } from "@/components/theme-picker"; import { CompletionDelaySetting } from "@/components/completion-delay-setting"; import { PushOptIn } from "@/components/push-opt-in"; import { NotifyChannelToggles } from "@/components/notify-channel-toggles"; import { ResponsiveSidebar } from "@/components/settings-section"; import type { SectionId } from "@/components/settings-section"; import { revokeShareLinkAction } from "./actions"; import { listCalendars } from "@/modules/calendar/server/queries"; import { listLists } from "@/modules/lists/server/queries"; import Link from "next/link"; import { NavIcon } from "@/components/nav-icon"; import { Mail, Globe, History, Sun, Bell, Pencil, Lock, Plus } from "lucide-react"; const VALID_SECTIONS = new Set([ "household", "sharing", "notifications", "calendars", "appearance", "data", ]); export default async function SettingsPage({ searchParams, }: { searchParams: Promise<{ s?: string }>; }) { const sp = await searchParams; const section: SectionId = VALID_SECTIONS.has(sp.s as SectionId) ? (sp.s as SectionId) : "household"; const { user, household } = await getCurrentSession(); const ntfyConfigured = !!(process.env["NTFY_URL"] && process.env["NTFY_TOPIC"]); const vapidKey = process.env["VAPID_PUBLIC_KEY"] ?? ""; return (
{section === "household" && } {section === "sharing" && } {section === "notifications" && ( )} {section === "calendars" && } {section === "appearance" && } {section === "data" && }
); } function HouseholdSection({ household, userName, }: { household: { id: string; name: string }; userName: string | null; }) { return ( <> {household.name} Self-hosted

Household name shown on share links and the iOS PWA.

Edit household & members
You
{(userName ?? "?").trim()[0]?.toUpperCase()}
{userName ?? "Anonymous"}
Signed in via Authentik
); } async function SharingSection() { const shareLinks = await getActiveShareLinks(); return ( Public share links

Share any calendar, list, note, or event with people outside the household. Links expire on their own — no logins needed.

{shareLinks.length === 0 ? (

No active share links.

) : (
{shareLinks.map((link) => { const registration = getEntityType(link.entityType); const label = registration?.label.singular ?? link.entityType; const iconName = link.entityType.startsWith("calendar.") ? "calendar" : link.entityType.startsWith("notes.") ? "note" : link.entityType.startsWith("lists.") ? "list" : "link"; return (
{label}
{link.capabilities.write ? "edit" : "view-only"} {link.expiresAt && ( · expires {link.expiresAt.toLocaleDateString()} )}
); })}
)}
); } function NotificationsSection({ user, vapidKey, ntfyConfigured, }: { user: { notifPush: boolean; notifInApp: boolean; notifNtfy: boolean }; vapidKey: string; ntfyConfigured: boolean; }) { return ( <> Push notifications Notification channels ); } async function CalendarsAndListsSection() { const [calendars, lists] = await Promise.all([listCalendars(), listLists()]); return ( <> Calendars New
{calendars.length === 0 ? (
No calendars yet.
) : ( calendars.map((c) => (
{c.name}
{c.visibility === "private" ? "Private" : "Household · everyone sees it"}
{c.visibility === "private" && }
)) )}
Lists
{lists.length === 0 ? (
No lists yet.
) : ( lists.map((l) => (
{l.name}
{l.type} · {l.openCount} open
)) )}
); } function AppearanceSection({ user, }: { user: { themePalette: string; themeMode: string; themeFontPair: string; themeDensity: string; themeDashLayout: string; themeCalView: string; themeNavStyle: string; }; }) { return ( Appearance ); } function DataSection() { return ( Data & backups
Auto-backup
Daily 03:00 → /var/backups/famapp/. Configured via host cron.
Server
Self-hosted via Docker Compose
Export
Not yet implemented — coming in v0.5
); }