refactor(web): decompose ProjectWizard into step components

Extract each wizard step into its own file under project-wizard/:
StepBar, DynamicFieldInput, Step1Identity, ResourcePersonPicker,
Step2Timeline, Step3Staffing, Step4Suggestions, Step5Review.
Main file reduced from 1,385 to 112 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:30:33 +02:00
parent f3fa902773
commit 85e1bcc06f
9 changed files with 1273 additions and 1285 deletions
@@ -0,0 +1,109 @@
import type { BlueprintFieldDefinition } from "@capakraken/shared";
import { FieldType } from "@capakraken/shared";
import { DateInput } from "~/components/ui/DateInput.js";
export function DynamicFieldInput({
field,
value,
onChange,
}: {
field: BlueprintFieldDefinition;
value: unknown;
onChange: (key: string, val: unknown) => void;
}) {
const strVal = value !== undefined && value !== null ? String(value) : "";
const arrVal = Array.isArray(value) ? (value as string[]) : [];
switch (field.type) {
case FieldType.TEXTAREA:
return (
<textarea
value={strVal}
onChange={(e) => onChange(field.key, e.target.value)}
placeholder={field.placeholder}
rows={3}
className="app-input"
/>
);
case FieldType.NUMBER:
return (
<input
type="number"
value={strVal}
min={field.validation?.min}
max={field.validation?.max}
onChange={(e) =>
onChange(field.key, e.target.value === "" ? "" : parseFloat(e.target.value))
}
placeholder={field.placeholder}
className="app-input"
/>
);
case FieldType.BOOLEAN:
return (
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={value === true || value === "true"}
onChange={(e) => onChange(field.key, e.target.checked)}
className="accent-brand-600"
/>
{field.description && <span className="text-gray-500">{field.description}</span>}
</label>
);
case FieldType.DATE:
return <DateInput value={strVal} onChange={(v) => onChange(field.key, v)} />;
case FieldType.SELECT:
return (
<select
value={strVal}
onChange={(e) => onChange(field.key, e.target.value)}
className="app-select w-full"
>
<option value=""> select </option>
{(field.options ?? []).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
case FieldType.MULTI_SELECT:
return (
<div className="flex flex-wrap gap-2">
{(field.options ?? []).map((opt) => {
const checked = arrVal.includes(opt.value);
return (
<label key={opt.value} className="flex items-center gap-1.5 text-sm cursor-pointer">
<input
type="checkbox"
checked={checked}
onChange={(e) => {
const next = e.target.checked
? [...arrVal, opt.value]
: arrVal.filter((v) => v !== opt.value);
onChange(field.key, next);
}}
className="accent-brand-600"
/>
{opt.label}
</label>
);
})}
</div>
);
default:
// TEXT, URL, EMAIL
return (
<input
type={
field.type === FieldType.EMAIL ? "email" : field.type === FieldType.URL ? "url" : "text"
}
value={strVal}
onChange={(e) => onChange(field.key, e.target.value)}
placeholder={field.placeholder}
className="app-input"
/>
);
}
}