From d78450fc47671b32aea9a16de07cc6aa65d7ae40 Mon Sep 17 00:00:00 2001 From: moh Date: Sun, 20 Sep 2026 01:56:50 +0200 Subject: [PATCH] FIXED - Make adding a portfolio project simpler and actually saveable - Fix critical bug: TabsContent unmounted inactive panels, so the project form only submitted the active tab's fields and could never save; add an opt-in forceMount that keeps panels mounted (hidden) and use it on all four project-form tabs - Make Sections and Assets optional: a project saves with just Basics and Localized Content; new projects start with no sections/assets, and the last one can now be removed - Update wizard progress so empty sections/assets steps count as complete - Update save hint copy and step labels to reflect the optional steps - Correct .claude/launch.json dev port to 3014 - Update portfolio-form-progress tests for the optional-steps behaviour --- .claude/launch.json | 2 +- components/admin/portfolio-project-form.tsx | 30 ++++++++++----------- components/ui/tabs.tsx | 8 +++++- lib/portfolio-form-progress.ts | 16 ++++++++--- tests/unit/portfolio-form-progress.test.ts | 25 ++++++++++++++--- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index 04ffa28..b30445f 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -5,7 +5,7 @@ "name": "dev", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], - "port": 3000 + "port": 3014 } ] } diff --git a/components/admin/portfolio-project-form.tsx b/components/admin/portfolio-project-form.tsx index 9c88fa6..0926108 100644 --- a/components/admin/portfolio-project-form.tsx +++ b/components/admin/portfolio-project-form.tsx @@ -165,13 +165,13 @@ const wizardSteps: Array<{ }, { key: "sections", - label: "Sections", - description: "Build the project story with ordered content blocks.", + label: "Sections (optional)", + description: "Optional — build the project story with ordered content blocks.", }, { key: "assets", - label: "Assets & Media", - description: "Cover and gallery assets from the media library.", + label: "Assets & Media (optional)", + description: "Optional — cover and gallery assets from the media library.", }, ]; @@ -233,7 +233,8 @@ function createInitialState( function createInitialSections(project: PortfolioProjectView | null | undefined): SectionFormValue[] { if (!project?.sections.length) { - return [createEmptySection(0)]; + // Sections are optional — start empty so a project can be saved without them. + return []; } return project.sections.map((section, index) => ({ @@ -259,7 +260,8 @@ function createInitialSections(project: PortfolioProjectView | null | undefined) function createInitialAssets(project: PortfolioProjectView | null | undefined): AssetFormValue[] { if (!project?.assets.length) { - return [createEmptyAsset(0)]; + // Gallery assets are optional — start empty so a project can be saved without them. + return []; } return project.assets.map((asset, index) => ({ @@ -732,7 +734,7 @@ export function PortfolioProjectForm({
- +
- +
- +
- sections.length > 1 && setSections((current) => current.filter((_, currentIndex) => currentIndex !== index), ) } - disabled={sections.length === 1} > @@ -1137,7 +1137,7 @@ export function PortfolioProjectForm({ - +
@@ -1228,12 +1228,10 @@ export function PortfolioProjectForm({ variant="ghost" className="text-destructive" onClick={() => - assets.length > 1 && setAssets((current) => current.filter((_, currentIndex) => currentIndex !== index), ) } - disabled={assets.length === 1} > @@ -1347,11 +1345,11 @@ export function PortfolioProjectForm({
{firstIncompleteStep ? (

- Save becomes available after all steps are complete. + Fill Basics and Localized Content to save. Sections and assets are optional.

) : (

- All steps are ready. You can save now. + Ready to save. Sections and assets are optional.

)} diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index cf742e2..025f4a0 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -98,22 +98,28 @@ function TabsTrigger({ type TabsContentProps = React.HTMLAttributes & { value: string; + // Keep the panel mounted while inactive (hidden via `hidden`) so its form + // fields still submit. Use for tabbed forms that share one submit button. + forceMount?: boolean; }; function TabsContent({ className, value, children, + forceMount = false, ...props }: TabsContentProps) { const { value: activeValue } = useTabsContext(); + const isActive = activeValue === value; - if (activeValue !== value) { + if (!isActive && !forceMount) { return null; } return (