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 (
0 && completedSections === input.sections.length,
- summary: `${completedSections}/${input.sections.length} sections ready.`,
+ // Optional: no sections is valid. If sections were added, each must be ready.
+ complete: completedSections === input.sections.length,
+ summary:
+ input.sections.length === 0
+ ? "Optional — no sections added."
+ : `${completedSections}/${input.sections.length} sections ready.`,
},
{
key: "assets",
- complete: input.assets.length > 0 && completedAssets === input.assets.length,
- summary: `${completedAssets}/${input.assets.length} assets ready.`,
+ // Optional: no gallery assets is valid. If assets were added, each must be ready.
+ complete: completedAssets === input.assets.length,
+ summary:
+ input.assets.length === 0
+ ? "Optional — no gallery assets added."
+ : `${completedAssets}/${input.assets.length} assets ready.`,
},
];
}
diff --git a/tests/unit/portfolio-form-progress.test.ts b/tests/unit/portfolio-form-progress.test.ts
index 7d27720..70e6733 100644
--- a/tests/unit/portfolio-form-progress.test.ts
+++ b/tests/unit/portfolio-form-progress.test.ts
@@ -117,10 +117,29 @@ describe("getPortfolioWizardProgress", () => {
expect(getFirstIncompleteWizardStep(progress)).toBe("content");
});
- it("sections/assets steps require at least one ready entry", () => {
+ it("treats sections/assets as optional: empty steps are complete and the project is saveable", () => {
const noEntries = getPortfolioWizardProgress({ ...completeInput, sections: [], assets: [] });
- expect(noEntries.find((s) => s.key === "sections")?.complete).toBe(false);
- expect(noEntries.find((s) => s.key === "assets")?.complete).toBe(false);
+ expect(noEntries.find((s) => s.key === "sections")?.complete).toBe(true);
+ expect(noEntries.find((s) => s.key === "assets")?.complete).toBe(true);
+ // Basics + content only is enough to save.
+ expect(getFirstIncompleteWizardStep(noEntries)).toBeNull();
+ });
+
+ it("shows an optional summary when no sections/assets are added", () => {
+ const noEntries = getPortfolioWizardProgress({ ...completeInput, sections: [], assets: [] });
+ expect(noEntries.find((s) => s.key === "sections")?.summary).toBe("Optional — no sections added.");
+ expect(noEntries.find((s) => s.key === "assets")?.summary).toBe(
+ "Optional — no gallery assets added.",
+ );
+ });
+
+ it("blocks saving when an added section is incomplete", () => {
+ const progress = getPortfolioWizardProgress({
+ ...completeInput,
+ sections: [section({ titleEn: "" })],
+ });
+ expect(progress.find((s) => s.key === "sections")?.complete).toBe(false);
+ expect(getFirstIncompleteWizardStep(progress)).toBe("sections");
});
it("summarizes section/asset readiness counts", () => {