// The editor — URL `/`. Introspects the api's registered content types // (`content.types`) and generates a form per type with @voltro/cms's // ``; lists a type's drafts live (`content.list`), saves them // (`content.saveDraft`, with per-field validation surfaced inline), and // publishes / unpublishes them. One page, no dynamic routes — the selected type // and the row being edited are component state. import type { ReactNode } from 'react' import { useEffect, useMemo, useState, type FormEvent } from 'react' import type { PageMeta } from '@voltro/web' import { useAction, useMutation, useSubscription } from '@voltro/client' import { ContentForm } from '@voltro/cms/web' import { T, useT } from '@voltro/i18n' import { getCatalog } from '../../locales' import { toContentFormType, type ContentRow, type ContentTypeDescriptor, type SaveResult, } from '../../lib/api' export const renderMode = 'ssr' as const export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({ title: getCatalog(locale)['meta.editor.title'], }) interface Violation { readonly field: string; readonly rule: string; readonly message: string } export default function Editor(): ReactNode { const types = useAction, { types: ReadonlyArray }>( 'cms', 'content.types', ) const getOne = useAction<{ type: string; id: string; status: string }, { row: Record | null }>( 'cms', 'content.get', ) const save = useMutation<{ type: string; values: Record }, SaveResult>('cms', 'content.saveDraft') const publish = useMutation<{ type: string; id: string }, { ok: boolean }>('cms', 'content.publish') const unpublish = useMutation<{ type: string; id: string }, { ok: boolean }>('cms', 'content.unpublish') const [typeList, setTypeList] = useState>([]) const [active, setActive] = useState(null) const [values, setValues] = useState>({}) const [editingId, setEditingId] = useState(null) const [violations, setViolations] = useState>([]) const [saved, setSaved] = useState(false) // Load the content types once; select the first. useEffect(() => { void types.run({}).then((r) => { setTypeList(r.types) setActive((cur) => cur ?? r.types[0]?.name ?? null) }) }, []) // mount-only fetch of the content-type descriptors const activeType = typeList.find((t) => t.name === active) ?? null // Live drafts of the active type. Memoised input so a stable object identity // avoids resubscribing every render; an unset/unknown type yields an empty list. const listInput = useMemo(() => ({ type: active ?? '—', status: 'draft' as const }), [active]) const { data: rows } = useSubscription>('cms', 'content.list', listInput) const drafts = rows ?? [] const resetForm = (): void => { setValues({}); setEditingId(null); setViolations([]) } const onSelectType = (name: string): void => { setActive(name); resetForm(); setSaved(false) } const onEdit = (id: string): void => { if (active === null) return void getOne.run({ type: active, id, status: 'draft' }).then((r) => { if (r.row) { setValues(r.row); setEditingId(id); setViolations([]); setSaved(false) } }) } const onSubmit = (e: FormEvent): void => { e.preventDefault() if (active === null) return const payload = editingId ? { ...values, id: editingId } : values void save.mutate({ type: active, values: payload }).then((res) => { if (res.ok) { resetForm(); setSaved(true) } else { setViolations(res.violations); setSaved(false) } }) } const onPublish = (id: string): void => { if (active) void publish.mutate({ type: active, id }) } const onUnpublish = (id: string): void => { if (active) void unpublish.mutate({ type: active, id }) } const statusKey = (s: string): 'editor.status.draft' | 'editor.status.published' | 'editor.status.archived' => s === 'published' ? 'editor.status.published' : s === 'archived' ? 'editor.status.archived' : 'editor.status.draft' if (typeList.length === 0) { return

} return (
{/* Left: type chooser + live draft list */}

{drafts.length === 0 ? (

) : (
    {drafts.map((row) => (
  • {row.title ?? row.slug ?? row.id}
  • ))}
)}
{/* Right: the auto-generated form */}

{editingId ? : }

{activeType ? (
{violations.length > 0 ? (
    {violations.map((v) =>
  • {v.field}: {v.message}
  • )}
) : null} {saved ?

: null}
{editingId ? ( ) : null}
) : null}
) }