124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
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),
|
|
});
|