diff --git a/src/app/components/system-config/BackupTab.tsx b/src/app/components/system-config/BackupTab.tsx new file mode 100644 index 0000000..262f0bc --- /dev/null +++ b/src/app/components/system-config/BackupTab.tsx @@ -0,0 +1,7 @@ +"use client"; + +import type { TabProps } from "./SystemConfigDialog"; + +export function BackupTab(_props: TabProps) { + return

Implemented in the next task.

; +} diff --git a/src/app/components/system-config/GeneralTab.tsx b/src/app/components/system-config/GeneralTab.tsx new file mode 100644 index 0000000..3951eb2 --- /dev/null +++ b/src/app/components/system-config/GeneralTab.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useRef, useState } from "react"; +import { mutate as globalMutate } from "swr"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { errorToast } from "@/lib/errorReporter"; +import { + PUBLIC_SYSTEM_CONFIG_KEY, + logoSrc, + usePublicSystemConfig, +} from "@/app/hooks/usePublicSystemConfig"; +import type { TabProps } from "./SystemConfigDialog"; + +function BrandingUpload({ + kind, + label, + currentSrc, +}: { + kind: "logo" | "favicon"; + label: string; + currentSrc: string; +}) { + const inputRef = useRef(null); + const [preview, setPreview] = useState(null); + const [busy, setBusy] = useState(false); + + const call = async (url: string, init: RequestInit, fallback: string) => { + setBusy(true); + try { + const response = await fetch(url, init); + const body = (await response.json().catch(() => null)) as + | { message?: string } + | null; + if (!response.ok) throw new Error(body?.message || fallback); + setPreview(null); + if (inputRef.current) inputRef.current.value = ""; + void globalMutate(PUBLIC_SYSTEM_CONFIG_KEY); + void globalMutate("/api/system/config"); + } catch (error) { + errorToast(`branding.${kind}`, error instanceof Error ? error.message : fallback); + } finally { + setBusy(false); + } + }; + + const upload = () => { + const file = inputRef.current?.files?.[0]; + if (!file) return; + const form = new FormData(); + form.set("kind", kind); + form.set("file", file); + void call("/api/system/branding", { method: "POST", body: form }, "Upload failed."); + }; + + return ( +
+ {`${label} +
+ + { + const file = event.target.files?.[0]; + setPreview(file ? URL.createObjectURL(file) : null); + }} + /> +
+ + +
+ ); +} + +export function GeneralTab({ draft, setDraft }: TabProps) { + const publicConfig = usePublicSystemConfig(); + return ( +
+
+ + + setDraft((config) => ({ + ...config, + branding: { ...config.branding, wordmark: event.target.value }, + })) + } + /> +
+ + 0 + ? `/api/system/branding/asset/favicon?v=${publicConfig.faviconVersion}` + : "/favicon.ico" + } + /> +

+ PNG, JPEG or SVG, up to 512KB. The favicon also accepts ICO. Changes + apply on save for the wordmark; uploads apply immediately. +

+
+ ); +} diff --git a/src/app/components/system-config/LoginTermsTab.tsx b/src/app/components/system-config/LoginTermsTab.tsx new file mode 100644 index 0000000..4ab3682 --- /dev/null +++ b/src/app/components/system-config/LoginTermsTab.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { Label } from "@/components/ui/label"; +import { MarkdownContent } from "@/app/components/MarkdownContent"; +import type { TabProps } from "./SystemConfigDialog"; + +export function LoginTermsTab({ draft, setDraft }: TabProps) { + return ( +
+ +
+ +