Files
sass-mohfarawati/lib/portfolio-form-progress.ts
T
moh d78450fc47 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
2026-09-20 01:56:50 +02:00

163 lines
4.4 KiB
TypeScript

import type { PortfolioProjectViewMode, PortfolioSectionType } from "@/lib/db/enums";
export type PortfolioWizardStep = "basics" | "content" | "sections" | "assets";
export type PortfolioWizardStepState = {
key: PortfolioWizardStep;
complete: boolean;
summary: string;
};
type ProjectBasicsProgressInput = {
categoryId: string;
slug: string;
clientName: string;
projectYear: string;
sortOrder: string;
viewMode: PortfolioProjectViewMode;
};
type ProjectContentProgressInput = {
titleAr: string;
titleEn: string;
titleDe: string;
serviceLabelAr: string;
serviceLabelEn: string;
serviceLabelDe: string;
summaryAr: string;
summaryEn: string;
summaryDe: string;
};
type ProjectSectionProgressInput = {
type: PortfolioSectionType;
titleAr: string;
titleEn: string;
titleDe: string;
bodyAr: string;
bodyEn: string;
bodyDe: string;
linkUrl: string;
mediaAssetId: string;
};
type ProjectAssetProgressInput = {
mediaAssetId: string;
altAr: string;
altEn: string;
altDe: string;
};
type PortfolioWizardProgressInput = {
basics: ProjectBasicsProgressInput;
content: ProjectContentProgressInput;
sections: ProjectSectionProgressInput[];
assets: ProjectAssetProgressInput[];
};
function hasText(value: string) {
return value.trim().length > 0;
}
function isValidSlug(value: string) {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.trim());
}
function isValidYear(value: string) {
const year = Number(value);
return Number.isInteger(year) && year >= 2000 && year <= 2100;
}
function isValidSortOrder(value: string) {
const sortOrder = Number(value);
return Number.isInteger(sortOrder) && sortOrder >= 0 && sortOrder <= 9999;
}
export function isPortfolioSectionReady(section: ProjectSectionProgressInput) {
const hasTitles = hasText(section.titleAr) && hasText(section.titleEn) && hasText(section.titleDe);
if (!hasTitles) {
return false;
}
if (section.type === "GALLERY") {
return hasText(section.mediaAssetId);
}
if (section.type === "LINK") {
return hasText(section.linkUrl);
}
return hasText(section.bodyAr) && hasText(section.bodyEn) && hasText(section.bodyDe);
}
export function isPortfolioAssetReady(asset: ProjectAssetProgressInput) {
return (
hasText(asset.mediaAssetId) &&
hasText(asset.altAr) &&
hasText(asset.altEn) &&
hasText(asset.altDe)
);
}
export function getPortfolioWizardProgress(
input: PortfolioWizardProgressInput,
): PortfolioWizardStepState[] {
const completedSections = input.sections.filter(isPortfolioSectionReady).length;
const completedAssets = input.assets.filter(isPortfolioAssetReady).length;
return [
{
key: "basics",
complete:
hasText(input.basics.categoryId) &&
isValidSlug(input.basics.slug) &&
hasText(input.basics.clientName) &&
isValidYear(input.basics.projectYear) &&
isValidSortOrder(input.basics.sortOrder) &&
Boolean(input.basics.viewMode),
summary: "Category, slug, client, year, and order are set.",
},
{
key: "content",
complete:
hasText(input.content.titleAr) &&
hasText(input.content.titleEn) &&
hasText(input.content.titleDe) &&
hasText(input.content.serviceLabelAr) &&
hasText(input.content.serviceLabelEn) &&
hasText(input.content.serviceLabelDe) &&
hasText(input.content.summaryAr) &&
hasText(input.content.summaryEn) &&
hasText(input.content.summaryDe),
summary: "Title, service label, and summary exist in all locales.",
},
{
key: "sections",
// 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",
// 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.`,
},
];
}
export function getFirstIncompleteWizardStep(
steps: PortfolioWizardStepState[],
): PortfolioWizardStep | null {
return steps.find((step) => !step.complete)?.key ?? null;
}