162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { Trash2, Plus, ChevronUp, ChevronDown } from 'lucide-react'
|
|
|
|
interface TemplateItemRow {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
}
|
|
|
|
interface TemplateSectionRow {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
items: TemplateItemRow[]
|
|
}
|
|
|
|
interface Props {
|
|
sections: TemplateSectionRow[]
|
|
onChange: (sections: TemplateSectionRow[]) => void
|
|
}
|
|
|
|
function uid() {
|
|
return Math.random().toString(36).slice(2)
|
|
}
|
|
|
|
export function TemplateSectionBuilder({ sections, onChange }: Props) {
|
|
function addSection() {
|
|
onChange([...sections, { id: uid(), title: '', description: '', items: [] }])
|
|
}
|
|
|
|
function removeSection(idx: number) {
|
|
onChange(sections.filter((_, i) => i !== idx))
|
|
}
|
|
|
|
function moveSection(idx: number, dir: -1 | 1) {
|
|
const next = [...sections]
|
|
const [removed] = next.splice(idx, 1)
|
|
next.splice(idx + dir, 0, removed)
|
|
onChange(next)
|
|
}
|
|
|
|
function updateSection(idx: number, field: 'title' | 'description', value: string) {
|
|
onChange(sections.map((s, i) => (i === idx ? { ...s, [field]: value } : s)))
|
|
}
|
|
|
|
function addItem(sIdx: number) {
|
|
onChange(sections.map((s, i) =>
|
|
i === sIdx ? { ...s, items: [...s.items, { id: uid(), title: '', description: '' }] } : s,
|
|
))
|
|
}
|
|
|
|
function removeItem(sIdx: number, iIdx: number) {
|
|
onChange(sections.map((s, i) =>
|
|
i === sIdx ? { ...s, items: s.items.filter((_, j) => j !== iIdx) } : s,
|
|
))
|
|
}
|
|
|
|
function moveItem(sIdx: number, iIdx: number, dir: -1 | 1) {
|
|
onChange(sections.map((s, i) => {
|
|
if (i !== sIdx) return s
|
|
const next = [...s.items]
|
|
const [removed] = next.splice(iIdx, 1)
|
|
next.splice(iIdx + dir, 0, removed)
|
|
return { ...s, items: next }
|
|
}))
|
|
}
|
|
|
|
function updateItem(sIdx: number, iIdx: number, field: 'title' | 'description', value: string) {
|
|
onChange(sections.map((s, i) =>
|
|
i === sIdx
|
|
? { ...s, items: s.items.map((item, j) => (j === iIdx ? { ...item, [field]: value } : item)) }
|
|
: s,
|
|
))
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{sections.map((section, sIdx) => (
|
|
<div key={section.id} className="border rounded-lg overflow-hidden">
|
|
<div className="bg-muted/40 px-3 py-2 flex items-center gap-2">
|
|
<div className="flex flex-col gap-0.5">
|
|
<Button type="button" variant="ghost" size="icon" className="h-4 w-4" disabled={sIdx === 0} onClick={() => moveSection(sIdx, -1)}>
|
|
<ChevronUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button type="button" variant="ghost" size="icon" className="h-4 w-4" disabled={sIdx === sections.length - 1} onClick={() => moveSection(sIdx, 1)}>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
<Input
|
|
className="h-7 text-sm font-medium flex-1"
|
|
placeholder="Section title *"
|
|
value={section.title}
|
|
onChange={(e) => updateSection(sIdx, 'title', e.target.value)}
|
|
required
|
|
/>
|
|
<Button type="button" variant="ghost" size="icon" className="h-7 w-7" onClick={() => removeSection(sIdx)}>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="p-3 space-y-2">
|
|
<Textarea
|
|
className="text-xs resize-none"
|
|
placeholder="Section description (optional)"
|
|
rows={1}
|
|
value={section.description}
|
|
onChange={(e) => updateSection(sIdx, 'description', e.target.value)}
|
|
/>
|
|
|
|
<div className="space-y-1.5">
|
|
{section.items.map((item, iIdx) => (
|
|
<div key={item.id} className="flex items-center gap-2">
|
|
<div className="flex flex-col gap-0.5">
|
|
<Button type="button" variant="ghost" size="icon" className="h-4 w-4" disabled={iIdx === 0} onClick={() => moveItem(sIdx, iIdx, -1)}>
|
|
<ChevronUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button type="button" variant="ghost" size="icon" className="h-4 w-4" disabled={iIdx === section.items.length - 1} onClick={() => moveItem(sIdx, iIdx, 1)}>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
<Input
|
|
className="h-7 text-xs flex-1"
|
|
placeholder="Item title *"
|
|
value={item.title}
|
|
onChange={(e) => updateItem(sIdx, iIdx, 'title', e.target.value)}
|
|
required
|
|
/>
|
|
<Input
|
|
className="h-7 text-xs flex-1"
|
|
placeholder="Description (optional)"
|
|
value={item.description}
|
|
onChange={(e) => updateItem(sIdx, iIdx, 'description', e.target.value)}
|
|
/>
|
|
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={() => removeItem(sIdx, iIdx)}>
|
|
<Trash2 className="h-3 w-3 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Button type="button" variant="outline" size="sm" className="text-xs h-7" onClick={() => addItem(sIdx)}>
|
|
<Plus className="h-3 w-3 mr-1" />Add Item
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<Button type="button" variant="outline" size="sm" onClick={addSection}>
|
|
<Plus className="h-4 w-4 mr-1" />Add Section
|
|
</Button>
|
|
|
|
{sections.length === 0 && (
|
|
<p className="text-sm text-muted-foreground text-center py-2">No sections yet — add one above.</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export type { TemplateSectionRow, TemplateItemRow }
|