/** * General Settings sub-page * * Site Identity (title, tagline, URL, logo, favicon) and Reading settings * (posts per page, date format, timezone). */ import { Button, Input, Label } from "@cloudflare/kumo"; import { useLingui } from "@lingui/react/macro"; import { FloppyDisk, CheckCircle, WarningCircle, Upload, X } from "@phosphor-icons/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as React from "react"; import { fetchSettings, updateSettings, type SiteSettings, type MediaItem } from "../../lib/api"; import { EditorHeader } from "../EditorHeader"; import { MediaPickerModal } from "../MediaPickerModal"; import { BackToSettingsLink } from "./BackToSettingsLink.js"; export function GeneralSettings() { const { t } = useLingui(); const queryClient = useQueryClient(); const { data: settings, isLoading } = useQuery({ queryKey: ["settings"], queryFn: fetchSettings, staleTime: Infinity, }); const [formData, setFormData] = React.useState>({}); const [saveStatus, setSaveStatus] = React.useState<{ type: "success" | "error"; message: string; } | null>(null); const [logoPickerOpen, setLogoPickerOpen] = React.useState(false); const [faviconPickerOpen, setFaviconPickerOpen] = React.useState(false); React.useEffect(() => { if (settings) setFormData(settings); }, [settings]); React.useEffect(() => { if (saveStatus) { const timer = setTimeout(setSaveStatus, 3000, null); return () => clearTimeout(timer); } }, [saveStatus]); const saveMutation = useMutation({ mutationFn: (data: Partial) => updateSettings(data), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["settings"] }); setSaveStatus({ type: "success", message: t`Settings saved successfully` }); }, onError: (error) => { setSaveStatus({ type: "error", message: error instanceof Error ? error.message : t`Failed to save settings`, }); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); saveMutation.mutate(formData); }; const handleChange = (key: keyof SiteSettings, value: unknown) => { setFormData((prev) => ({ ...prev, [key]: value })); }; const handleLogoSelect = (media: MediaItem) => { setFormData((prev) => ({ ...prev, logo: { mediaId: media.id, alt: media.alt || "", url: media.url }, })); setLogoPickerOpen(false); }; const handleFaviconSelect = (media: MediaItem) => { setFormData((prev) => ({ ...prev, favicon: { mediaId: media.id, url: media.url }, })); setFaviconPickerOpen(false); }; const handleLogoRemove = () => { setFormData((prev) => ({ ...prev, logo: undefined })); }; const handleFaviconRemove = () => { setFormData((prev) => ({ ...prev, favicon: undefined })); }; if (isLoading) { return (

{t`General Settings`}

{t`Loading settings...`}

); } return (
{/* Sticky header — keeps Save in view while users scroll a long settings form. The bottom "Save Settings" button is preserved below so the natural last-control DOM order works for keyboard and screen-reader users. */} } actions={ } >

{t`General Settings`}

{/* Status banner */} {saveStatus && (
{saveStatus.type === "success" ? ( ) : ( )} {saveStatus.message}
)}
{/* Site Identity */}

{t`Site Identity`}

handleChange("title", e.target.value)} description={t`The name of your site, used in the header and metadata`} /> handleChange("tagline", e.target.value)} description={t`A short description of your site`} /> handleChange("url", e.target.value)} description={t`The public URL of your site (used for canonical links and sitemaps)`} /> {/* Logo Picker -- "configured" gates on `mediaId`, not `url`, so an orphaned reference (media row deleted, or a stale provider id stored pre-localOnly fix) still renders Remove. Otherwise the user would see "Select Logo" and silently re-save the dangling `mediaId` on any unrelated change. */}
{formData.logo?.mediaId ? (
{formData.logo.url ? ( {formData.logo.alt ) : (
)}
) : ( )}
{/* Favicon Picker — see Logo Picker for the orphan-state rationale. */}
{formData.favicon?.mediaId ? (
{formData.favicon.url ? ( {t`Favicon`} ) : (
)}
) : ( )}
{/* Reading Settings */}

{t`Reading`}

handleChange("postsPerPage", parseInt(e.target.value, 10))} min={1} max={100} description={t`Number of posts to show per page on list views`} /> handleChange("dateFormat", e.target.value)} description={`Example: ${formData.dateFormat || "MMMM d, yyyy"} → January 23, 2026`} /> handleChange("timezone", e.target.value)} description={t`Timezone for displaying dates (e.g., America/New_York)`} />
{/* Save Button */}
{/* Media Picker Modals -- localOnly: site settings only persist a local `mediaId`. URL/provider selections would be stripped on save, leaving an unresolvable reference. See MediaPickerModalProps.localOnly. */}
); } export default GeneralSettings;