'use client'; /** * Editor for content/privacy.md (#699), in Settings → Legal. * * Folio writes no legal text. Next to the editor it lists what this * installation processes, read off the configuration, and "Insert headings" * adds the section headings that list implies, nothing more. * * Saves on its own, like the About page: the policy is a Markdown file, not a * key in settings.yaml. */ import { useEffect, useState } from 'react'; import * as Icons from './Icons'; import { reportIfSessionExpired } from './sessionExpiry'; import { useUnsavedGuard } from './useUnsavedGuard'; import type { ProcessingFact } from '@/lib/privacy'; interface PrivacyData { body: string; enabled: boolean; facts: ProcessingFact[]; starter: string; } type Status = { kind: 'success' | 'error'; message: string } | null; export default function PrivacyEditor() { const [data, setData] = useState(null); const [loadError, setLoadError] = useState(null); const [body, setBody] = useState(''); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(null); useUnsavedGuard(dirty); useEffect(() => { let live = true; fetch('/api/admin/privacy') .then(async (res) => { if (!res.ok) { reportIfSessionExpired(res); throw new Error(`HTTP ${res.status}`); } return (await res.json()) as PrivacyData; }) .then((d) => { if (!live) return; setData(d); setBody(d.body); }) .catch((err: Error) => live && setLoadError(err.message)); return () => { live = false; }; }, []); async function save() { setSaving(true); setStatus(null); try { const res = await fetch('/api/admin/privacy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }), }); if (!res.ok) { if (!reportIfSessionExpired(res)) { setStatus({ kind: 'error', message: `Could not save (HTTP ${res.status}).` }); } return; } setDirty(false); setStatus({ kind: 'success', message: 'Privacy policy saved.' }); } catch { setStatus({ kind: 'error', message: 'Could not reach the server.' }); } finally { setSaving(false); } } function insertHeadings() { if (!data) return; setBody((b) => (b.trim() ? `${b.trimEnd()}\n\n${data.starter}\n` : `${data.starter}\n`)); setDirty(true); } if (loadError) { // No editor over an unknown file: saving an empty one would erase the policy. return (

The privacy policy could not be loaded ({loadError}). Reload the page to try again.

); } if (!data) return
; return (

What this site processes

Read off your current configuration. A checklist for writing the policy, not text for the page.

    {data.facts.map((f) => (
  • {f.topic} {f.thirdParty && third party} {f.detail}
  • ))}