import { PortfolioAssetKind, PortfolioSectionType } from "@prisma/client"; import { z } from "zod"; import { mediaFieldInputSchema } from "./media-validation"; const requiredText = (label: string) => z .string() .trim() .min(1, `${label} is required.`); const optionalTrimmedText = z.string().trim().optional().transform((value) => value ?? ""); export const categoryInputSchema = z.object({ id: z.string().trim().optional(), slug: requiredText("Category slug") .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Category slug must be lowercase and hyphenated."), nameAr: requiredText("Category nameAr"), nameEn: requiredText("Category nameEn"), nameDe: requiredText("Category nameDe"), descriptionAr: requiredText("Category descriptionAr"), descriptionEn: requiredText("Category descriptionEn"), descriptionDe: requiredText("Category descriptionDe"), sortOrder: z.coerce.number().int().min(0).max(9999), isActive: z.boolean(), }); export const sectionInputSchema = z .object({ id: z.string().trim().optional(), type: z.nativeEnum(PortfolioSectionType), titleAr: requiredText("Section titleAr"), titleEn: requiredText("Section titleEn"), titleDe: requiredText("Section titleDe"), bodyAr: optionalTrimmedText, bodyEn: optionalTrimmedText, bodyDe: optionalTrimmedText, imagePath: optionalTrimmedText, media: mediaFieldInputSchema.optional(), linkUrl: optionalTrimmedText.refine( (value) => value === "" || /^https?:\/\//.test(value) || value.startsWith("/"), "Section linkUrl must be an absolute URL or start with /.", ), sortOrder: z.coerce.number().int().min(0).max(9999), }) .superRefine((value, context) => { const hasBody = Boolean(value.bodyAr && value.bodyEn && value.bodyDe); const hasImage = value.media?.mode === "library" ? Boolean(value.media.assetId) : value.media?.mode === "upload" ? true : Boolean(value.imagePath); if ( (value.type === PortfolioSectionType.RICH_TEXT || value.type === PortfolioSectionType.STATS || value.type === PortfolioSectionType.DELIVERABLES) && !hasBody ) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["bodyAr"], message: "This section type requires body content in all languages.", }); } if (value.type === PortfolioSectionType.GALLERY && !hasImage) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["media"], message: "Single image sections require an image.", }); } if (value.type === PortfolioSectionType.LINK && !value.linkUrl) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["linkUrl"], message: "Link sections require a link URL.", }); } }); export const assetInputSchema = z.object({ id: z.string().trim().optional(), kind: z.nativeEnum(PortfolioAssetKind), filePath: optionalTrimmedText, fileFieldName: optionalTrimmedText, media: mediaFieldInputSchema.optional(), altAr: requiredText("Asset altAr"), altEn: requiredText("Asset altEn"), altDe: requiredText("Asset altDe"), sortOrder: z.coerce.number().int().min(0).max(9999), }); export const projectInputSchema = z.object({ id: z.string().trim().optional(), categoryId: requiredText("Project categoryId"), slug: requiredText("Project slug") .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Project slug must be lowercase and hyphenated."), titleAr: requiredText("Project titleAr"), titleEn: requiredText("Project titleEn"), titleDe: requiredText("Project titleDe"), summaryAr: requiredText("Project summaryAr"), summaryEn: requiredText("Project summaryEn"), summaryDe: requiredText("Project summaryDe"), clientName: requiredText("Project clientName"), projectYear: z.coerce.number().int().min(2000).max(2100), serviceLabelAr: requiredText("Project serviceLabelAr"), serviceLabelEn: requiredText("Project serviceLabelEn"), serviceLabelDe: requiredText("Project serviceLabelDe"), previewUrl: optionalTrimmedText.refine( (value) => value === "" || /^https?:\/\//.test(value), "Project previewUrl must be an absolute URL.", ), currentCoverImagePath: optionalTrimmedText, coverMedia: mediaFieldInputSchema.optional(), sortOrder: z.coerce.number().int().min(0).max(9999), isFeatured: z.boolean(), isPublished: z.boolean(), sections: z.array(sectionInputSchema), assets: z.array(assetInputSchema), });