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:
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user