/** * Sections library page component * * Browse, create, and manage reusable content sections (block patterns). */ import { Button, Dialog, Input, InputArea, Select, Toast } from "@cloudflare/kumo"; import type { MessageDescriptor } from "@lingui/core"; import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import { Plus, MagnifyingGlass, Trash, PencilSimple, Copy, FolderOpen, Globe, User, FileArrowDown, } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import * as React from "react"; import { fetchSections, createSection, deleteSection, type Section, type SectionSource, } from "../lib/api"; import { slugify } from "../lib/utils"; import { ConfirmDialog } from "./ConfirmDialog.js"; import { DialogError, getMutationError } from "./DialogError.js"; const sourceIcons: Record = { theme: Globe, user: User, import: FileArrowDown, }; const sourceLabels: Record = { theme: msg`Theme`, user: msg`Custom`, import: msg`Imported`, }; export function Sections() { const { t } = useLingui(); const navigate = useNavigate(); const queryClient = useQueryClient(); const toastManager = Toast.useToastManager(); const [isCreateOpen, setIsCreateOpen] = React.useState(false); const [deleteSlug, setDeleteSlug] = React.useState(null); const [searchQuery, setSearchQuery] = React.useState(""); const [selectedSource, setSelectedSource] = React.useState(null); // Create form state const [createTitle, setCreateTitle] = React.useState(""); const [createSlug, setCreateSlug] = React.useState(""); const [createDescription, setCreateDescription] = React.useState(""); const [slugTouched, setSlugTouched] = React.useState(false); const [createError, setCreateError] = React.useState(null); // Reset form when dialog closes React.useEffect(() => { if (!isCreateOpen) { setCreateTitle(""); setCreateSlug(""); setCreateDescription(""); setSlugTouched(false); setCreateError(null); } }, [isCreateOpen]); const { data: sectionsData, isLoading: sectionsLoading } = useQuery({ queryKey: ["sections", { source: selectedSource, search: searchQuery }], queryFn: () => fetchSections({ source: selectedSource || undefined, search: searchQuery || undefined, }), }); const sections = sectionsData?.items ?? []; const createMutation = useMutation({ mutationFn: createSection, onSuccess: (section) => { void queryClient.invalidateQueries({ queryKey: ["sections"] }); setIsCreateOpen(false); toastManager.add({ title: t`Section created` }); // Navigate to edit the new section void navigate({ to: "/sections/$slug", params: { slug: section.slug } }); }, onError: (error: Error) => { setCreateError(error.message); }, }); const deleteMutation = useMutation({ mutationFn: deleteSection, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["sections"] }); setDeleteSlug(null); toastManager.add({ title: t`Section deleted` }); }, }); const handleCreate = (e: React.FormEvent) => { e.preventDefault(); setCreateError(null); createMutation.mutate({ slug: createSlug, title: createTitle, description: createDescription || undefined, content: [], // Start with empty content }); }; const handleCopySlug = (slug: string) => { void navigator.clipboard.writeText(slug); toastManager.add({ title: t`Slug copied to clipboard` }); }; const sectionToDelete = sections.find((s) => s.slug === deleteSlug); return (
{/* Header */}

{t`Sections`}

{t`Reusable content blocks you can insert into any content`}

( )} />
{t`Create Section`} ( )} />
{ const title = e.target.value; setCreateTitle(title); if (!slugTouched && title) { setCreateSlug(slugify(title)); } }} required placeholder={t`Hero Banner`} />
{ setCreateSlug(e.target.value); setSlugTouched(true); }} required placeholder="hero-banner" pattern="[a-z0-9\-]+" title={t`Lowercase letters, numbers, and hyphens only`} />

{t`Used to identify this section. Lowercase letters, numbers, and hyphens only.`}

setCreateDescription(e.target.value)} placeholder={t`A full-width hero banner with heading, text, and CTA button`} rows={3} />
{/* Filters */}
{/* Search */}
setSearchQuery(e.target.value)} className="ps-10" />
{/* Source filter */}