fix update Section Block

This commit is contained in:
Your Name committed 2026-06-17 21:08:24 -04:00
1 parent 996dfdabd0
commit 6f67ee6077
47 files changed
+1792 -197

No files matched your search

+153 -17
View File
@@ -1,6 +1,7 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import {
LocalizedStringSchema,
HeroSectionPayloadSchema,
CategoriesSectionPayloadSchema,
ProductsSectionPayloadSchema,
@@ -21,6 +22,122 @@ import {
} from "@homaroma/types";
import { adminOnly } from "../middleware/auth";
function isLocalizedStringValue(value: unknown): value is string | Record<string, string> {
if (typeof value === "string") return true;
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
return Object.values(value).every((entry) => typeof entry === "string");
}
function isLocalizedStringList(value: unknown): boolean {
return Array.isArray(value) && value.every((entry) => isLocalizedStringValue(entry));
}
function hasValidString(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0;
}
function hasValidNumber(value: unknown): boolean {
return typeof value === "number" && Number.isFinite(value);
}
function hasValidBoolean(value: unknown): boolean {
return typeof value === "boolean";
}
function validateHeroArticlePayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (!isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
const localizedFields = ["subtitle", "author", "category"] as const;
for (const field of localizedFields) {
if (payload[field] !== undefined && !isLocalizedStringValue(payload[field])) {
return `${field}: Must be a localized string`;
}
}
const stringFields = ["publishDate", "date", "imageUrl", "backgroundImageUrl", "overlayColor"] as const;
for (const field of stringFields) {
if (payload[field] !== undefined && !hasValidString(payload[field])) {
return `${field}: Must be a string`;
}
}
return null;
}
function validateRichTextPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
if (!isLocalizedStringValue(payload.content)) return "content: Must be a localized string";
return null;
}
function validateScentNotesBreakdownPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
if (payload.description !== undefined && !isLocalizedStringValue(payload.description)) return "description: Must be a localized string";
const listFields = ["topNotes", "heartNotes", "middleNotes", "baseNotes"] as const;
for (const field of listFields) {
const value = payload[field];
if (value !== undefined && !(isLocalizedStringValue(value) || isLocalizedStringList(value))) {
return `${field}: Must be a localized string or localized string list`;
}
}
return null;
}
function validateQuoteCalloutPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (!isLocalizedStringValue(payload.quote)) return "quote: Must be a localized string";
if (payload.author !== undefined && !isLocalizedStringValue(payload.author)) return "author: Must be a localized string";
if (payload.role !== undefined && !isLocalizedStringValue(payload.role)) return "role: Must be a localized string";
return null;
}
function validateProductCarouselPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
if (payload.description !== undefined && !isLocalizedStringValue(payload.description)) return "description: Must be a localized string";
if (payload.collectionId !== undefined && !hasValidString(payload.collectionId)) return "collectionId: Must be a string";
if (payload.limit !== undefined && (!hasValidNumber(payload.limit) || payload.limit <= 0)) return "limit: Must be a positive number";
if (payload.showPrice !== undefined && !hasValidBoolean(payload.showPrice)) return "showPrice: Must be a boolean";
if (payload.showAddToCart !== undefined && !hasValidBoolean(payload.showAddToCart)) return "showAddToCart: Must be a boolean";
return null;
}
function validateAccordionPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
if (!Array.isArray(payload.categories) || payload.categories.length === 0) return "categories: Must be a non-empty array";
for (const [catIndex, category] of payload.categories.entries()) {
if (!category || typeof category !== "object") return `categories.${catIndex}: Must be an object`;
if (!isLocalizedStringValue(category.name)) return `categories.${catIndex}.name: Must be a localized string`;
if (category.icon !== undefined && !hasValidString(category.icon)) return `categories.${catIndex}.icon: Must be a string`;
if (!Array.isArray(category.questions) || category.questions.length === 0) {
return `categories.${catIndex}.questions: Must be a non-empty array`;
}
for (const [qIndex, question] of category.questions.entries()) {
if (!question || typeof question !== "object") return `categories.${catIndex}.questions.${qIndex}: Must be an object`;
if (!isLocalizedStringValue(question.q)) return `categories.${catIndex}.questions.${qIndex}.q: Must be a localized string`;
if (!isLocalizedStringValue(question.a)) return `categories.${catIndex}.questions.${qIndex}.a: Must be a localized string`;
}
}
return null;
}
function validateContactInfoPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
for (const field of ["address", "email", "line", "hours"] as const) {
if (!isLocalizedStringValue(payload[field])) return `${field}: Must be a localized string`;
}
return null;
}
function validateContactFormPayload(payload: any): string | null {
if (!payload || typeof payload !== "object") return "Payload must be a JSON object";
if (payload.title !== undefined && !isLocalizedStringValue(payload.title)) return "title: Must be a localized string";
if (payload.subtitle !== undefined && !isLocalizedStringValue(payload.subtitle)) return "subtitle: Must be a localized string";
return null;
}
const RESERVED_SLUGS = [
"shop", "category", "products", "cart", "checkout", "account", "search",
"login", "admin", "wishlist", "track", "coupons", "scent-guide",
@@ -28,6 +145,19 @@ const RESERVED_SLUGS = [
"settings", "menus", "auth", "api"
];
/** Resolve category name — mirrors the same helper in categories.ts */
function resolveCatName(cat: any, lang: string): any {
if (lang === 'all') {
const nameObj: any = {};
cat.translations?.forEach((tr: any) => { nameObj[tr.langCode] = tr.name; });
return nameObj;
}
const trans = cat.translations?.find((t: any) => t.langCode === lang)
|| cat.translations?.find((t: any) => t.langCode === 'th')
|| cat.translations?.[0];
return trans?.name ?? cat.name;
}
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
@@ -125,6 +255,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
categories = await prisma.category.findMany({
where: { id: { in: ids } },
include: {
translations: true,
_count: {
select: { products: true }
}
@@ -134,13 +265,18 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
} else {
categories = await prisma.category.findMany({
include: {
translations: true,
_count: {
select: { products: true }
}
}
});
}
populatedPayload.categories = categories;
// Resolve name for each category using the lang param (supports 'all')
populatedPayload.categories = categories.map((cat: any) => ({
...cat,
name: resolveCatName(cat, lang),
}));
}
return {
@@ -452,43 +588,43 @@ function validateSectionPayload(type: string, payload: any): string | null {
break;
}
case 'HERO_ARTICLE': {
const res = HeroArticlePayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateHeroArticlePayload(payload);
if (error) return error;
break;
}
case 'RICH_TEXT': {
const res = RichTextPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateRichTextPayload(payload);
if (error) return error;
break;
}
case 'SCENT_NOTES_BREAKDOWN': {
const res = ScentNotesBreakdownPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateScentNotesBreakdownPayload(payload);
if (error) return error;
break;
}
case 'QUOTE_CALLOUT': {
const res = QuoteCalloutPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateQuoteCalloutPayload(payload);
if (error) return error;
break;
}
case 'PRODUCT_CAROUSEL': {
const res = ProductCarouselPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateProductCarouselPayload(payload);
if (error) return error;
break;
}
case 'ACCORDION': {
const res = AccordionSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateAccordionPayload(payload);
if (error) return error;
break;
}
case 'CONTACT_INFO': {
const res = ContactInfoSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateContactInfoPayload(payload);
if (error) return error;
break;
}
case 'CONTACT_FORM': {
const res = ContactFormSectionPayloadSchema.safeParse(payload);
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
const error = validateContactFormPayload(payload);
if (error) return error;
break;
}
default: