"use client"; import { useState } from "react"; import { FieldType } from "@capakraken/shared"; import type { BlueprintFieldDefinition, FieldOption, StaffingRequirement } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { RolePresetsEditor } from "./RolePresetsEditor.js"; const FIELD_TYPES: { value: FieldType; label: string }[] = [ { value: FieldType.TEXT, label: "Text" }, { value: FieldType.TEXTAREA, label: "Textarea" }, { value: FieldType.NUMBER, label: "Number" }, { value: FieldType.BOOLEAN, label: "Boolean" }, { value: FieldType.DATE, label: "Date" }, { value: FieldType.SELECT, label: "Select" }, { value: FieldType.MULTI_SELECT, label: "Multi-Select" }, { value: FieldType.URL, label: "URL" }, { value: FieldType.EMAIL, label: "Email" }, ]; const BTN_PRIMARY = "px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"; const BTN_SECONDARY = "px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium"; function makeEmptyField(order: number): BlueprintFieldDefinition { return { id: Math.random().toString(36).slice(2), key: "", label: "", type: FieldType.TEXT, required: false, order, }; } // --------------------------------------------------------------------------- // OptionsEditor — for SELECT / MULTI_SELECT // --------------------------------------------------------------------------- interface OptionsEditorProps { options: FieldOption[]; onChange: (options: FieldOption[]) => void; } function OptionsEditor({ options, onChange }: OptionsEditorProps) { function addOption() { onChange([...options, { value: "", label: "" }]); } function removeOption(idx: number) { onChange(options.filter((_, i) => i !== idx)); } function updateOption(idx: number, field: "value" | "label", val: string) { const next = options.map((o, i) => i === idx ? { ...o, [field]: val } : o, ); onChange(next); } return (

Options

{options.map((opt, idx) => (
updateOption(idx, "value", e.target.value)} placeholder="value" className="app-input flex-1" /> updateOption(idx, "label", e.target.value)} placeholder="label" className="app-input flex-1" />
))}
); } // --------------------------------------------------------------------------- // FieldRow — a single field definition row // --------------------------------------------------------------------------- interface FieldRowProps { field: BlueprintFieldDefinition; onChange: (field: BlueprintFieldDefinition) => void; onDelete: () => void; } function FieldRow({ field, onChange, onDelete }: FieldRowProps) { const [expanded, setExpanded] = useState(false); const needsOptions = field.type === FieldType.SELECT || field.type === FieldType.MULTI_SELECT; function update( key: K, value: BlueprintFieldDefinition[K], ) { onChange({ ...field, [key]: value }); } return (
{/* Main row */}
{/* Drag handle placeholder */} {/* Key */} update("key", e.target.value)} placeholder="field_key" className="app-input w-36 font-mono" aria-label="Field key" /> {/* Label */} update("label", e.target.value)} placeholder="Label" className="app-input w-40" aria-label="Field label" /> {/* Type */} {/* Required */} {/* Expand/Collapse optional fields */} {/* Delete */}
{/* Expanded optional fields */} {expanded && (
update("group", e.target.value || undefined)} placeholder="Section heading" className="app-input" />
update("placeholder", e.target.value || undefined) } placeholder="Placeholder text" className="app-input" />
update("description", e.target.value || undefined) } placeholder="Helper text" className="app-input" />
{needsOptions && (
update("options", opts)} />
)}
)} {/* Options inline hint when collapsed */} {!expanded && needsOptions && (field.options?.length ?? 0) === 0 && (

No options defined yet — click ▼ more to add them.

)}
); } // --------------------------------------------------------------------------- // BlueprintFieldEditor — the modal // --------------------------------------------------------------------------- interface BlueprintFieldEditorProps { blueprintId: string; blueprintName: string; initialFieldDefs: BlueprintFieldDefinition[]; initialRolePresets?: StaffingRequirement[]; initialTab?: "fields" | "presets"; onClose: () => void; } export function BlueprintFieldEditor({ blueprintId, blueprintName, initialFieldDefs, initialRolePresets = [], initialTab = "fields", onClose, }: BlueprintFieldEditorProps) { const utils = trpc.useUtils(); const [activeTab, setActiveTab] = useState<"fields" | "presets">(initialTab); const [fields, setFields] = useState( () => [...initialFieldDefs].sort((a, b) => a.order - b.order), ); const [saveError, setSaveError] = useState(null); const [presetSaveError, setPresetSaveError] = useState(null); const updateMutation = trpc.blueprint.update.useMutation(); const presetMutation = trpc.blueprint.updateRolePresets.useMutation(); function addField() { setFields((prev) => [...prev, makeEmptyField(prev.length)]); } function removeField(idx: number) { setFields((prev) => prev .filter((_, i) => i !== idx) .map((f, i) => ({ ...f, order: i })), ); } function updateField(idx: number, updated: BlueprintFieldDefinition) { setFields((prev) => prev.map((f, i) => (i === idx ? updated : f)), ); } function handleSave() { setSaveError(null); // Reassign order by current list position const normalised = fields.map((f, i) => ({ ...f, order: i })); updateMutation.mutate( { id: blueprintId, data: { fieldDefs: normalised }, }, { onSuccess: async () => { await utils.blueprint.list.invalidate(); onClose(); }, onError: (err) => { setSaveError(err.message); }, }, ); } // Close on backdrop click function handleBackdropClick(e: React.MouseEvent) { if (e.target === e.currentTarget) onClose(); } return (
{/* Header */}

Edit Fields:{" "} {blueprintName}

{/* Tabs */}
{(["fields", "presets"] as const).map((tab) => ( ))}
{activeTab === "fields" ? ( <> {/* Field list */}
{fields.length === 0 && (

No fields yet. Click “+ Add Field” to get started.

)} {fields.map((field, idx) => ( updateField(idx, updated)} onDelete={() => removeField(idx)} /> ))}
{/* Add field button */}
{/* Error */} {saveError && (
{saveError}
)} {/* Footer */}
) : (

Role presets are auto-loaded in Step 3 of the Project Creation Wizard when this blueprint is selected.

presetMutation.mutate( { id: blueprintId, rolePresets: presets }, { onSuccess: async () => { await utils.blueprint.list.invalidate(); setPresetSaveError(null); onClose(); }, onError: (err) => { setPresetSaveError(err.message); }, }, ) } isSaving={presetMutation.isPending} saveError={presetSaveError} />
)}
); }