- Add lib/db (Drizzle schema: 7 tables + 6 enums + relations, postgres.js client), drizzle.config.ts, lib/db/enums.ts (Prisma-compatible enum objects) - Rewrite all 14 app consumers + 4 admin components to Drizzle - Move the test DB layer to Drizzle + in-process PGlite; convert all 8 integration test files + factories (371 tests green) - Remove Prisma: deps, prisma/ schema+migrations, lib/prisma.ts, Dockerfile prisma generate; wire db:generate/migrate/push/studio to drizzle-kit; update Makefile - Preserve the old seed as scripts/legacy-prisma-seed.cjs (needs a Drizzle rewrite)
155 lines
4.1 KiB
TypeScript
155 lines
4.1 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",
|
|
complete: input.sections.length > 0 && completedSections === input.sections.length,
|
|
summary: `${completedSections}/${input.sections.length} sections ready.`,
|
|
},
|
|
{
|
|
key: "assets",
|
|
complete: input.assets.length > 0 && completedAssets === input.assets.length,
|
|
summary: `${completedAssets}/${input.assets.length} assets ready.`,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function getFirstIncompleteWizardStep(
|
|
steps: PortfolioWizardStepState[],
|
|
): PortfolioWizardStep | null {
|
|
return steps.find((step) => !step.complete)?.key ?? null;
|
|
}
|