ADDED - Save an unfinished project as a draft

- Add a Save as draft button that bypasses the completeness gate: the user
  can save partial work and finish later
- projectDraftInputSchema makes the user-facing copy (title, summary, service
  label, client) optional; the action auto-generates a slug from the title
  (or draft-<timestamp>), defaults the year to the current year, and forces
  the project unpublished
- Drafts drop not-yet-valid sections/assets instead of failing the save
- Pass intent via a hidden field set on click so the server reliably sees it
- Tests: draft schema accepts empty copy a strict save rejects, and still
  requires a slug and a valid year
This commit is contained in:
moh
2026-09-20 16:52:13 +02:00
parent 39e459fa09
commit 4b535694bc
4 changed files with 164 additions and 14 deletions
+27 -4
View File
@@ -347,16 +347,26 @@ function SectionHeader({
);
}
function SubmitButton() {
function SubmitButton({ onSelect }: { onSelect: () => void }) {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
<Button type="submit" onClick={onSelect} disabled={pending}>
{pending ? "Saving..." : "Save Project"}
</Button>
);
}
function DraftButton({ onSelect }: { onSelect: () => void }) {
const { pending } = useFormStatus();
return (
<Button type="submit" onClick={onSelect} variant="outline" disabled={pending}>
Save as draft
</Button>
);
}
function ViewModeCard({
active,
label,
@@ -466,6 +476,12 @@ export function PortfolioProjectForm({
const [showValidation, setShowValidation] = useState(false);
const sectionsInputRef = useRef<HTMLInputElement | null>(null);
const assetsInputRef = useRef<HTMLInputElement | null>(null);
const intentRef = useRef<HTMLInputElement | null>(null);
const setIntent = (value: "save" | "draft") => {
if (intentRef.current) {
intentRef.current.value = value;
}
};
const sectionsPayload = JSON.stringify(sections.map((section, index) => ({ ...section, sortOrder: index })));
const assetsPayload = JSON.stringify(assets.map((asset, index) => ({ ...asset, sortOrder: index })));
@@ -540,6 +556,11 @@ export function PortfolioProjectForm({
action={action}
className="space-y-8"
onSubmit={(event) => {
// A draft save skips the completeness gate entirely.
if (intentRef.current?.value === "draft") {
return;
}
if (!firstIncompleteStep) {
return;
}
@@ -554,6 +575,7 @@ export function PortfolioProjectForm({
<input type="hidden" name="viewMode" value={projectState.viewMode} />
<input ref={sectionsInputRef} type="hidden" name="sections" value={sectionsPayload} />
<input ref={assetsInputRef} type="hidden" name="assets" value={assetsPayload} />
<input ref={intentRef} type="hidden" name="intent" defaultValue="save" />
<AppCard level={3} layer="single" padding="lg" className="space-y-6">
<div className="flex flex-col gap-6">
@@ -1186,14 +1208,15 @@ export function PortfolioProjectForm({
<div className="flex flex-wrap items-center gap-3">
{firstIncompleteStep ? (
<p className="text-sm text-muted-foreground">
Fill Basics and Localized Content to save. Sections and assets are optional.
Fill Basics and Localized Content to publish or save as a draft anytime.
</p>
) : (
<p className="text-sm text-muted-foreground">
Ready to save. Sections and assets are optional.
</p>
)}
<SubmitButton />
<DraftButton onSelect={() => setIntent("draft")} />
<SubmitButton onSelect={() => setIntent("save")} />
</div>
</div>
</AppCard>