since3-portal/modules/templates/ui/components/editor/content-panel.tsx
2026-10-11 18:22:32 +02:00

134 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useId } from "react"
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, Trash2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import type { FieldDef, ListFieldDef, ListItem, TemplateData, TemplateDefinition } from "../../../lib/types"
const MAX_ITEMS = 40
export function ContentPanel({ def, data, hidden, onChange }: {
def: TemplateDefinition
data: TemplateData
hidden: string[]
onChange: (section: string, field: string, value: string | ListItem[]) => void
}) {
return (
<div className="flex flex-col gap-2">
{def.sections.map((section, index) => (
// Im Baukasten stehen die Bausteine im Vordergrund – „Marke & Kontakt“ startet zugeklappt
<Collapsible key={section.key} defaultOpen={index === 0 && !def.builder} className="rounded-lg border">
<CollapsibleTrigger className="flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-2.5 text-left text-sm font-medium [&[data-panel-open]>svg]:rotate-180">
<span className="flex items-center gap-2">
{section.label}
{hidden.includes(section.key) && (
<span className="rounded bg-surface px-1.5 py-0.5 text-[11px] font-normal text-muted-foreground">ausgeblendet</span>
)}
</span>
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform" />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-4 border-t px-3 py-4">
{section.fields.map((field) =>
field.type === "list" ? (
<ListEditor
key={field.key}
field={field}
items={(data[section.key]?.[field.key] as ListItem[] | undefined) ?? []}
onChange={(items) => onChange(section.key, field.key, items)}
/>
) : (
<FieldInput
key={field.key}
field={field}
value={(data[section.key]?.[field.key] as string | undefined) ?? ""}
onChange={(value) => onChange(section.key, field.key, value)}
/>
)
)}
</div>
</CollapsibleContent>
</Collapsible>
))}
</div>
)
}
export function FieldInput({ field, value, onChange }: { field: FieldDef; value: string; onChange: (value: string) => void }) {
const id = useId()
return (
<Field>
<FieldLabel htmlFor={id}>{field.label}</FieldLabel>
{field.type === "textarea" ? (
<Textarea id={id} value={value} rows={3} onChange={(event) => onChange(event.target.value)} />
) : (
<Input
id={id}
value={value}
type={field.type === "image" ? "url" : "text"}
placeholder={field.type === "image" ? "https://images.unsplash.com/…" : undefined}
onChange={(event) => onChange(event.target.value)}
/>
)}
{field.type === "image" && value && (
<div aria-hidden className="h-20 rounded-md border bg-surface bg-cover bg-center" style={{ backgroundImage: `url("${value}")` }} />
)}
{field.hint && <FieldDescription>{field.hint}</FieldDescription>}
</Field>
)
}
export function ListEditor({ field, items, onChange }: { field: ListFieldDef; items: ListItem[]; onChange: (items: ListItem[]) => void }) {
function update(index: number, key: string, value: string) {
onChange(items.map((item, i) => (i === index ? { ...item, [key]: value } : item)))
}
function move(index: number, offset: number) {
const next = [...items]
const [item] = next.splice(index, 1)
next.splice(index + offset, 0, item)
onChange(next)
}
return (
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">{field.label}</p>
{items.map((item, index) => (
<div key={index} className="rounded-md border bg-surface/50 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">{field.itemLabel} {index + 1}</span>
<div className="flex">
<Button variant="ghost" size="icon-sm" aria-label="Nach oben" disabled={index === 0} onClick={() => move(index, -1)}>
<ChevronUpIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Nach unten" disabled={index === items.length - 1} onClick={() => move(index, 1)}>
<ChevronDownIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label={`${field.itemLabel} entfernen`} onClick={() => onChange(items.filter((_, i) => i !== index))}>
<Trash2Icon />
</Button>
</div>
</div>
<div className="flex flex-col gap-3">
{field.fields.map((sub) => (
<FieldInput key={sub.key} field={sub} value={item[sub.key] ?? ""} onChange={(value) => update(index, sub.key, value)} />
))}
</div>
</div>
))}
<Button
variant="outline"
size="sm"
disabled={items.length >= MAX_ITEMS}
onClick={() => onChange([...items, Object.fromEntries(field.fields.map((sub) => [sub.key, ""]))])}
>
<PlusIcon /> {field.itemLabel} hinzufügen
</Button>
</div>
)
}