This commit is contained in:
Your Name committed 2026-10-03 15:09:45 +07:00
1 parent b8a255821b
commit 46f09ab145
16 files changed
+2269 -186

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
import {
PAGE_SECTION_PAYLOAD_SCHEMAS,
CATEGORY_SECTION_TYPES,
type CategorySectionType,
} from "@homaroma/types";
import type { z } from "zod";
export type CategorySectionValidationResult =
| { success: true; type: CategorySectionType; data: unknown }
| { success: false; issues: z.ZodIssue[] };
export function defaultCategorySections(categoryId: number) {
return [
{
categoryId,
type: "CATEGORY_HEADER",
order: 1,
payload: {
showBreadcrumbs: true,
showCategoryNav: true,
title: { th: "", en: "" },
subtitle: { th: "", en: "" },
showHeading: true,
},
isActive: true,
isLocked: false,
},
{
categoryId,
type: "CATALOG_GRID",
order: 2,
payload: {
showFilters: true,
showToolbar: true,
itemsPerPage: 24,
},
isActive: true,
isLocked: false,
},
{
categoryId,
type: "USP_BAR",
order: 3,
payload: {
showUsp: true,
},
isActive: true,
isLocked: false,
},
];
}
interface CategorySectionTransaction {
categorySection: {
count(args: any): Promise<number>;
createMany(args: any): Promise<unknown>;
findFirst(args: any): Promise<{ id?: number; order: number; type?: string } | null>;
update(args: any): Promise<any>;
updateMany(args: any): Promise<unknown>;
create(args: any): Promise<any>;
};
}
interface CreateCategorySectionInput {
categoryId: number;
type: CategorySectionType;
payload: unknown;
order?: number;
isActive: boolean;
isLocked: boolean;
}
export async function createCategorySection(
tx: CategorySectionTransaction,
input: CreateCategorySectionInput,
) {
const existingCount = await tx.categorySection.count({
where: { categoryId: input.categoryId },
});
if (existingCount === 0) {
await tx.categorySection.createMany({
data: defaultCategorySections(input.categoryId),
});
const seededSection = await tx.categorySection.findFirst({
where: { categoryId: input.categoryId, type: input.type },
});
if (seededSection?.id !== undefined) {
return await tx.categorySection.update({
where: { id: seededSection.id },
data: {
payload: input.payload,
isActive: input.isActive,
isLocked: input.isLocked,
...(input.order !== undefined ? { order: input.order } : {}),
},
});
}
}
const lastSection = await tx.categorySection.findFirst({
where: { categoryId: input.categoryId },
orderBy: { order: "desc" },
});
return await tx.categorySection.create({
data: {
categoryId: input.categoryId,
type: input.type,
order: input.order ?? (lastSection?.order || 0) + 1,
payload: input.payload,
isActive: input.isActive,
isLocked: input.isLocked,
},
});
}
export function isCategorySectionType(
type: string,
): type is CategorySectionType {
return (CATEGORY_SECTION_TYPES as readonly string[]).includes(type);
}
export function createDefaultCategorySectionPayload(type: string): Record<string, unknown> {
const normalizedType = type.toUpperCase();
switch (normalizedType) {
case "CATEGORY_HEADER":
return {
showBreadcrumbs: true,
showCategoryNav: true,
title: { th: "", en: "" },
subtitle: { th: "", en: "" },
showHeading: true,
};
case "CATALOG_GRID":
return {
showFilters: true,
showToolbar: true,
itemsPerPage: 24,
};
case "USP_BAR":
return {
showUsp: true,
};
case "HERO_CAROUSEL":
return {
slides: [
{
imageUrl: "",
mobileImageUrl: "",
linkUrl: "",
alt: { th: "", en: "" },
title: { th: "", en: "" },
subtitle: { th: "", en: "" },
buttonText: { th: "", en: "" },
focalPoint: { x: "50%", y: "50%" },
},
],
autoplay: true,
interval: 4500,
aspectRatio: "21/7",
mobileAspectRatio: "16/9",
showArrows: true,
showDots: true,
};
case "DIVIDER":
return {
width: "container",
style: "solid",
thickness: 1,
spacing: "md",
color: "#94a3b8",
};
default:
return {};
}
}
export function validateCategorySectionPayload(
type: string,
payload: unknown,
): CategorySectionValidationResult {
const normalizedType = type.toUpperCase();
if (!isCategorySectionType(normalizedType)) {
return {
success: false,
issues: [
{
code: "custom",
message: `Unknown category section type: ${type}`,
path: [],
},
] as z.ZodIssue[],
};
}
let finalPayload = payload;
if (!finalPayload || typeof finalPayload !== "object" || Array.isArray(finalPayload) || Object.keys(finalPayload).length === 0) {
finalPayload = createDefaultCategorySectionPayload(normalizedType);
}
if (normalizedType === "HERO_CAROUSEL" && finalPayload && typeof finalPayload === "object") {
const slides = (finalPayload as any).slides;
if (Array.isArray(slides)) {
finalPayload = {
...finalPayload,
slides: slides.map((s: any) => ({
...s,
imageUrl: (typeof s?.imageUrl === "string" && s.imageUrl.trim()) ? s.imageUrl.trim() : "/placeholder.svg",
})),
};
}
}
const schema = (PAGE_SECTION_PAYLOAD_SCHEMAS as any)[normalizedType];
if (!schema) {
return { success: true, type: normalizedType, data: finalPayload };
}
const result = schema.safeParse(finalPayload);
if (!result.success) {
return { success: false, issues: result.error.issues };
}
return { success: true, type: normalizedType, data: result.data };
}
+1
View File
@@ -20,3 +20,4 @@ const prisma = new PrismaClient({
});
export default prisma;
// Reloaded with CategorySection support