diff --git a/apps/admin/src/components/page-builder/ProductPickerModal.tsx b/apps/admin/src/components/page-builder/ProductPickerModal.tsx new file mode 100644 index 0000000..231fc27 --- /dev/null +++ b/apps/admin/src/components/page-builder/ProductPickerModal.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; +import { X, Search, Loader2 } from 'lucide-react'; +import { API_BASE, type Product } from '@homaroma/types'; +import { getAdminDisplayString } from '../../utils/localization'; + +interface ProductPickerModalProps { + selectedIds: number[]; + onSave: (ids: number[]) => void; + onClose: () => void; +} + +export default function ProductPickerModal({ + selectedIds, + onSave, + onClose, +}: ProductPickerModalProps) { + const [search, setSearch] = useState(''); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState>(new Set(selectedIds)); + + useEffect(() => { + const fetchProducts = async () => { + setLoading(true); + try { + const url = `${API_BASE}/products?limit=20&search=${encodeURIComponent(search)}`; + const res = await fetch(url); + const data = await res.json(); + if (data && data.data) { + setProducts(data.data); + } else if (data && data.products) { + setProducts(data.products); + } else if (Array.isArray(data)) { + setProducts(data); + } else { + setProducts([]); + } + } catch (err) { + console.error('Fetch products error:', err); + } finally { + setLoading(false); + } + }; + + const timer = setTimeout(() => { + fetchProducts(); + }, 300); // debounce + + return () => clearTimeout(timer); + }, [search]); + + const toggleSelect = (id: number) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const handleSave = () => { + onSave(Array.from(selected)); + onClose(); + }; + + return ( +
+
+
+
+

Select Products

+ +
+ +
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + } + }} + className="w-full bg-slate-800 border border-white/10 text-white rounded-xl pl-10 pr-4 py-2 text-sm focus:outline-none focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow transition-all" + /> +
+
+ +
+ {loading ? ( +
+ +
+ ) : products.length > 0 ? ( + products.map((product) => { + const isSelected = selected.has(product.id); + const name = getAdminDisplayString(product.name); + return ( +
toggleSelect(product.id)} + className={`flex items-center p-3 rounded-xl cursor-pointer border transition-all ${ + isSelected + ? 'bg-cyan-glow/10 border-cyan-glow/30' + : 'bg-white/5 border-transparent hover:bg-white/10 hover:border-white/20' + }`} + > +
+

{name}

+ {product.sku &&

SKU: {product.sku}

} +
+
+ {isSelected && ( + + + + )} +
+
+ ); + }) + ) : ( +
+ No products found. +
+ )} +
+ +
+ + {selected.size} product{selected.size !== 1 ? 's' : ''} selected + +
+ + +
+
+
+
+ ); +} diff --git a/apps/admin/src/components/page-builder/ProductsForm.tsx b/apps/admin/src/components/page-builder/ProductsForm.tsx index 2872fd6..558c2ce 100644 --- a/apps/admin/src/components/page-builder/ProductsForm.tsx +++ b/apps/admin/src/components/page-builder/ProductsForm.tsx @@ -1,11 +1,18 @@ -import React from 'react'; +import React, { useState } from 'react'; import { type PageSection } from '@homaroma/types'; +import ProductPickerModal from './ProductPickerModal'; +import { getAdminDisplayString } from '../../utils/localization'; interface Brand { id: number; name: string; } +interface Category { + id: number; + name: any; +} + interface ProductsFormProps { editingSection: Partial; setEditingSection: React.Dispatch | null>>; @@ -13,6 +20,7 @@ interface ProductsFormProps { getLocalizedValue: (payload: any, field: string) => string; updateLocalizedValue: (field: string, value: string) => void; brands: Brand[]; + categories?: Category[]; } export default function ProductsForm({ @@ -21,7 +29,10 @@ export default function ProductsForm({ getLocalizedValue, updateLocalizedValue, brands, + categories = [], }: ProductsFormProps) { + const [isPickerOpen, setIsPickerOpen] = useState(false); + return (
@@ -50,16 +61,17 @@ export default function ProductsForm({ - setEditingSection({ - ...editingSection, - payload: { ...(editingSection.payload || {}), layout: e.target.value } - }) + setEditingSection(prev => prev ? { + ...prev, + payload: { ...(prev.payload || {}), layout: e.target.value } + } : null) } className="admin-input w-full bg-slate-900 border-white/10" > @@ -110,10 +122,10 @@ export default function ProductsForm({ type="checkbox" checked={editingSection.payload?.enableQuickShop ?? false} onChange={(e) => - setEditingSection({ - ...editingSection, - payload: { ...(editingSection.payload || {}), enableQuickShop: e.target.checked } - }) + setEditingSection(prev => prev ? { + ...prev, + payload: { ...(prev.payload || {}), enableQuickShop: e.target.checked } + } : null) } className="accent-cyan-glow" /> @@ -122,16 +134,39 @@ export default function ProductsForm({
+ {editingSection.payload?.type === 'featured' && ( +
+ + +
+ )} + {editingSection.payload?.type === 'brand' && (
{ - const ids = e.target.value - .split(',') - .map((id) => parseInt(id.trim())) - .filter((id) => !isNaN(id)); - setEditingSection({ - ...editingSection, - payload: { ...(editingSection.payload || {}), productIds: ids } - }); - }} - className="admin-input w-full" - /> + +
+ + +
+ {Array.isArray(editingSection.payload?.productIds) && editingSection.payload.productIds.length > 0 && ( +
+ IDs: {editingSection.payload.productIds.join(', ')} +
+ )}
)} + + {isPickerOpen && ( + { + setEditingSection((prev) => prev ? { + ...prev, + payload: { ...(prev.payload || {}), productIds: ids } + } : null); + }} + onClose={() => setIsPickerOpen(false)} + /> + )} ); } diff --git a/apps/admin/src/pages/PageBuilderPage.tsx b/apps/admin/src/pages/PageBuilderPage.tsx index bb55242..e7c492b 100644 --- a/apps/admin/src/pages/PageBuilderPage.tsx +++ b/apps/admin/src/pages/PageBuilderPage.tsx @@ -706,7 +706,7 @@ export default function PageBuilderPage() { }); if (res.ok) { - setEditingSection(null); + // setEditingSection(null); // Fix: Do not close editing panel on save if (selectedPageId) fetchPageDetails(selectedPageId); } else { const errData = await res.json(); @@ -1070,6 +1070,7 @@ export default function PageBuilderPage() { getLocalizedValue={getLocalizedValue} updateLocalizedValue={updateLocalizedValue} brands={brands} + categories={categories} /> )} diff --git a/apps/backend/package.json b/apps/backend/package.json index 8dd4e2a..9ff2f7f 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -24,7 +24,8 @@ "bcryptjs": "^3.0.3", "dotenv": "^17.4.0", "elysia": "^1.2.25", - "pg": "^8.20.0" + "pg": "^8.20.0", + "zod": "^4.4.3" }, "devDependencies": { "@types/bcryptjs": "^3.0.0", diff --git a/apps/backend/src/routes/pages.ts b/apps/backend/src/routes/pages.ts index 9339aa0..70ffe5a 100644 --- a/apps/backend/src/routes/pages.ts +++ b/apps/backend/src/routes/pages.ts @@ -11,133 +11,18 @@ import { NewsletterSectionPayloadSchema, EditorialTileSectionPayloadSchema, EditorialCarouselSectionPayloadSchema, - HeroArticlePayloadSchema, - RichTextPayloadSchema, - ScentNotesBreakdownPayloadSchema, - QuoteCalloutPayloadSchema, - ProductCarouselPayloadSchema, + HeroArticleSectionPayloadSchema, + RichTextSectionPayloadSchema, + ScentNotesBreakdownSectionPayloadSchema, + QuoteCalloutSectionPayloadSchema, + ProductCarouselSectionPayloadSchema, AccordionSectionPayloadSchema, ContactInfoSectionPayloadSchema, ContactFormSectionPayloadSchema } from "@homaroma/types"; +import { z } from "zod"; import { adminOnly } from "../middleware/auth"; -function isLocalizedStringValue(value: unknown): value is string | Record { - 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", @@ -215,7 +100,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) if (payload.type === 'featured') { products = await prisma.product.findMany({ - where: { categoryId: 1 }, + where: { categoryId: Number(payload.categoryId) || 1 }, include: productInclude, take: limit }); @@ -273,7 +158,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) if (payload.type === 'featured' || payload.collectionId === 'featured') { products = await prisma.product.findMany({ - where: { categoryId: 1 }, + where: { categoryId: Number(payload.categoryId) || 1 }, include: productInclude, take: limit }); @@ -517,10 +402,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) const pageId = Number(slug); const { type, order, payload, isActive } = body as any; - const validationError = validateSectionPayload(type, payload); - if (validationError) { + const validationResult = validateSectionPayload(type, payload); + if (!validationResult.success) { set.status = 400; - return { error: validationError }; + return { error: "Validation failed", details: validationResult.issues }; } let finalOrder = order; @@ -569,10 +454,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) } validationType = existingSection.type; } - const validationError = validateSectionPayload(validationType, payload); - if (validationError) { + const validationResult = validateSectionPayload(validationType, payload); + if (!validationResult.success) { set.status = 400; - return { error: validationError }; + return { error: "Validation failed", details: validationResult.issues }; } } @@ -637,100 +522,38 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) } }); -function validateSectionPayload(type: string, payload: any): string | null { +function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } { if (!payload || typeof payload !== 'object') { - return "Payload must be a JSON object"; + return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] as z.ZodIssue[] }; } + let schema: z.ZodTypeAny; + switch (type.toUpperCase()) { - case 'HERO': { - const res = HeroSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'CATEGORIES': { - const res = CategoriesSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'PRODUCTS': { - const res = ProductsSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'BANNER': { - const res = BannerSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'TEXT': { - const res = TextSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'ANNOUNCEMENT_BAR': { - const res = AnnouncementBarSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'NEWSLETTER': { - const res = NewsletterSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'EDITORIAL_TILE': { - const res = EditorialTileSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'EDITORIAL_CAROUSEL': { - const res = EditorialCarouselSectionPayloadSchema.safeParse(payload); - if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); - break; - } - case 'HERO_ARTICLE': { - const error = validateHeroArticlePayload(payload); - if (error) return error; - break; - } - case 'RICH_TEXT': { - const error = validateRichTextPayload(payload); - if (error) return error; - break; - } - case 'SCENT_NOTES_BREAKDOWN': { - const error = validateScentNotesBreakdownPayload(payload); - if (error) return error; - break; - } - case 'QUOTE_CALLOUT': { - const error = validateQuoteCalloutPayload(payload); - if (error) return error; - break; - } - case 'PRODUCT_CAROUSEL': { - const error = validateProductCarouselPayload(payload); - if (error) return error; - break; - } - case 'ACCORDION': { - const error = validateAccordionPayload(payload); - if (error) return error; - break; - } - case 'CONTACT_INFO': { - const error = validateContactInfoPayload(payload); - if (error) return error; - break; - } - case 'CONTACT_FORM': { - const error = validateContactFormPayload(payload); - if (error) return error; - break; - } + case 'HERO': schema = HeroSectionPayloadSchema; break; + case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break; + case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break; + case 'BANNER': schema = BannerSectionPayloadSchema; break; + case 'TEXT': schema = TextSectionPayloadSchema; break; + case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break; + case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break; + case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break; + case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break; + case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break; + case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break; + case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break; + case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break; + case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break; + case 'ACCORDION': schema = AccordionSectionPayloadSchema; break; + case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break; + case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break; default: - return `Unknown section type: ${type}`; + return { success: false, issues: [{ code: 'custom', message: `Unknown section type: ${type}`, path: [] }] as z.ZodIssue[] }; } - return null; -} + const res = schema.safeParse(payload); + if (!res.success) { + return { success: false, issues: res.error.issues }; + } + return { success: true }; +} \ No newline at end of file diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index 8607c98..bc06277 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -217,6 +217,7 @@ function ProductsSection({ payload, lang }: { payload: Record, const resolvedLang = lang === 'all' ? i18nLang : lang const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Featured Picks' const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang) + const layout = typeof payload.layout === 'string' ? payload.layout : 'grid' const products = getProducts(payload) return ( @@ -232,9 +233,29 @@ function ProductsSection({ payload, lang }: { payload: Record, {products.length ? ( -
- {products.map((product) => )} -
+ layout === 'carousel' ? ( +
+ + {products.map((product) => ( + + + + ))} + +
+ ) : ( +
+ {products.map((product) => )} +
+ ) ) : (

{t('dynamic.emptyProducts', { lng: resolvedLang })} diff --git a/bun.lock b/bun.lock index cf5b93d..8dfb4ea 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "homaroma-monorepo", + "dependencies": { + "cheerio": "^1.2.0", + }, }, "apps/admin": { "name": "admin", @@ -53,6 +56,7 @@ "dotenv": "^17.4.0", "elysia": "^1.2.25", "pg": "^8.20.0", + "zod": "^4.4.3", }, "devDependencies": { "@types/bcryptjs": "^3.0.0", @@ -754,6 +758,8 @@ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -782,6 +788,10 @@ "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -816,6 +826,10 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -874,8 +888,16 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.4.10", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -894,8 +916,12 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], @@ -1090,6 +1116,8 @@ "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -1098,7 +1126,7 @@ "i18next": ["i18next@26.3.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1302,6 +1330,8 @@ "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -1340,6 +1370,12 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1604,6 +1640,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -1626,8 +1664,12 @@ "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -1716,6 +1758,8 @@ "backend/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "eslint-config-next/globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], @@ -1746,18 +1790,26 @@ "homaroma-website/react-i18next": ["react-i18next@15.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw=="], + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "is-bun-module/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "mysql2/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "sharp/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], diff --git a/fix_pages.py b/fix_pages.py new file mode 100644 index 0000000..7ba4366 --- /dev/null +++ b/fix_pages.py @@ -0,0 +1,230 @@ +import re + +filePath = 'apps/backend/src/routes/pages.ts' +with open(filePath, 'r') as f: + content = f.read() + +# 1. Update imports +old_imports = """import { + LocalizedStringSchema, + HeroSectionPayloadSchema, + CategoriesSectionPayloadSchema, + ProductsSectionPayloadSchema, + BannerSectionPayloadSchema, + TextSectionPayloadSchema, + AnnouncementBarSectionPayloadSchema, + NewsletterSectionPayloadSchema, + EditorialTileSectionPayloadSchema, + EditorialCarouselSectionPayloadSchema, + HeroArticlePayloadSchema, + RichTextPayloadSchema, + ScentNotesBreakdownPayloadSchema, + QuoteCalloutPayloadSchema, + ProductCarouselPayloadSchema, + AccordionSectionPayloadSchema, + ContactInfoSectionPayloadSchema, + ContactFormSectionPayloadSchema +} from "@homaroma/types";""" + +new_imports = """import { + LocalizedStringSchema, + HeroSectionPayloadSchema, + CategoriesSectionPayloadSchema, + ProductsSectionPayloadSchema, + BannerSectionPayloadSchema, + TextSectionPayloadSchema, + AnnouncementBarSectionPayloadSchema, + NewsletterSectionPayloadSchema, + EditorialTileSectionPayloadSchema, + EditorialCarouselSectionPayloadSchema, + HeroArticleSectionPayloadSchema, + RichTextSectionPayloadSchema, + ScentNotesBreakdownSectionPayloadSchema, + QuoteCalloutSectionPayloadSchema, + ProductCarouselSectionPayloadSchema, + AccordionSectionPayloadSchema, + ContactInfoSectionPayloadSchema, + ContactFormSectionPayloadSchema +} from "@homaroma/types"; +import { z } from "zod";""" + +content = content.replace(old_imports, new_imports) + +# 2. Remove manual validation functions +# From 'function isLocalizedStringValue' up to 'const RESERVED_SLUGS' +start_idx = content.find('function isLocalizedStringValue') +end_idx = content.find('const RESERVED_SLUGS') + +if start_idx != -1 and end_idx != -1: + content = content[:start_idx] + content[end_idx:] + +# 3. Refactor validateSectionPayload +old_validate_func = """function validateSectionPayload(type: string, payload: any): string | null { + if (!payload || typeof payload !== 'object') { + return "Payload must be a JSON object"; + } + + switch (type.toUpperCase()) { + case 'HERO': { + const res = HeroSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'CATEGORIES': { + const res = CategoriesSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'PRODUCTS': { + const res = ProductsSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'BANNER': { + const res = BannerSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'TEXT': { + const res = TextSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'ANNOUNCEMENT_BAR': { + const res = AnnouncementBarSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'NEWSLETTER': { + const res = NewsletterSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'EDITORIAL_TILE': { + const res = EditorialTileSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'EDITORIAL_CAROUSEL': { + const res = EditorialCarouselSectionPayloadSchema.safeParse(payload); + if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', '); + break; + } + case 'HERO_ARTICLE': { + const error = validateHeroArticlePayload(payload); + if (error) return error; + break; + } + case 'RICH_TEXT': { + const error = validateRichTextPayload(payload); + if (error) return error; + break; + } + case 'SCENT_NOTES_BREAKDOWN': { + const error = validateScentNotesBreakdownPayload(payload); + if (error) return error; + break; + } + case 'QUOTE_CALLOUT': { + const error = validateQuoteCalloutPayload(payload); + if (error) return error; + break; + } + case 'PRODUCT_CAROUSEL': { + const error = validateProductCarouselPayload(payload); + if (error) return error; + break; + } + case 'ACCORDION': { + const error = validateAccordionPayload(payload); + if (error) return error; + break; + } + case 'CONTACT_INFO': { + const error = validateContactInfoPayload(payload); + if (error) return error; + break; + } + case 'CONTACT_FORM': { + const error = validateContactFormPayload(payload); + if (error) return error; + break; + } + default: + return `Unknown section type: ${type}`; + } + + return null; +}""" + +new_validate_func = """function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } { + if (!payload || typeof payload !== 'object') { + return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] as z.ZodIssue[] }; + } + + let schema: z.ZodTypeAny; + + switch (type.toUpperCase()) { + case 'HERO': schema = HeroSectionPayloadSchema; break; + case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break; + case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break; + case 'BANNER': schema = BannerSectionPayloadSchema; break; + case 'TEXT': schema = TextSectionPayloadSchema; break; + case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break; + case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break; + case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break; + case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break; + case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break; + case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break; + case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break; + case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break; + case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break; + case 'ACCORDION': schema = AccordionSectionPayloadSchema; break; + case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break; + case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break; + default: + return { success: false, issues: [{ code: 'custom', message: `Unknown section type: ${type}`, path: [] }] as z.ZodIssue[] }; + } + + const res = schema.safeParse(payload); + if (!res.success) { + return { success: false, issues: res.error.issues }; + } + return { success: true }; +}""" + +# I need to use re.sub for validateSectionPayload in case of slight whitespace differences, but a simple find+replace is safer if exact. +# Let's just use re.sub with a regex that captures everything from `function validateSectionPayload(type: string, payload: any): string | null {` to the end of the file. +pattern = r'function validateSectionPayload\(type: string, payload: any\): string \| null \{[\s\S]*' +content = re.sub(pattern, new_validate_func, content) + + +# 4. Update error handling in POST and PUT +# POST +old_post_err = """ const validationError = validateSectionPayload(type, payload); + if (validationError) { + set.status = 400; + return { error: validationError }; + }""" +new_post_err = """ const validationResult = validateSectionPayload(type, payload); + if (!validationResult.success) { + set.status = 400; + return { error: "Validation failed", details: validationResult.issues }; + }""" +content = content.replace(old_post_err, new_post_err) + +# PUT +old_put_err = """ const validationError = validateSectionPayload(validationType, payload); + if (validationError) { + set.status = 400; + return { error: validationError }; + }""" +new_put_err = """ const validationResult = validateSectionPayload(validationType, payload); + if (!validationResult.success) { + set.status = 400; + return { error: "Validation failed", details: validationResult.issues }; + }""" +content = content.replace(old_put_err, new_put_err) + +with open(filePath, 'w') as f: + f.write(content) diff --git a/mega01.html b/mega01.html new file mode 100644 index 0000000..a6f56d4 --- /dev/null +++ b/mega01.html @@ -0,0 +1 @@ +Mega 01 | HomAroma

ค้นพบน้ำมันหอมระเหยกลิ่นลาเวนเดอร์ (Lavender Essential Oil) แท้ 100%

สัมผัสกลิ่น

รายละเอียดกลิ่น

กลิ่นเปิด

มะกรูด

กลิ่นกลาง

กุหลาบ

กลิ่นฐาน

มัสก์

สินค้าแนะนำ

ดูทั้งหมด →

ตะไคร้

ออยล์โอโซน

4 ตัวเลือก

222ป (Tested)

น้ำมันหอมระเหย

8 ตัวเลือก

ตะไคร้

ก้านไม้หอม

3 ตัวเลือก

ตะไคร้

สเปรย์ปรับอากาศ

฿150

ตะไคร้

น้ำหอมในรถยนต์

฿60

ติดต่อเรา

ส่งข้อความถึงเรา

ฮีโร่บทความ

หมวดหมู่

ฮีโร่บทความ

บทความหลัก

โดย นักเขียน / 2023-01-01

หัวข้อ

เนื้อหา

"

คำคม

ผู้แต่ง / บทบาท

คำถามทั่วไป

คำถาม

คำตอบ

ข้อมูลติดต่อ

ที่อยู่

ที่อยู่

Line

@example

เวลาทำการ

เวลาทำการ

Lavender [Essential Oil]

PURE CALM

Lavender Essential Oil

สัมผัสความผ่อนคลายด้วยน้ำมันหอมระเหยลาเวนเดอร์แท้จากธรรมชาติ

Lavender Special Offer

Lavender Special Offer

รับส่วนลด 15% สำหรับผลิตภัณฑ์กลิ่นลาเวนเดอร์ทุกชนิด

Shop Now

Stay Calm with Lavender

รับข่าวสารและเคล็ดลับการใช้งานน้ำมันหอมระเหยลาเวนเดอร์

เรื่องราวของลาเวนเดอร์

ลาเวนเดอร์เป็นพืชที่มีกลิ่นหอมเฉพาะตัวและมีคุณสมบัติช่วยให้ผ่อนคลาย นอนหลับสบาย

Lavender Products

ผลิตภัณฑ์ที่โดดเด่นของเรา

ดูทั้งหมด →
แหล่งกำเนิด [Lavender]

แหล่งกำเนิด Lavender

คัดสรรดอกลาเวนเดอร์จากแหล่งเพาะปลูกที่ดีที่สุด เพื่อกลิ่นหอมที่บริสุทธิ์และผ่อนคลาย

อ่านเพิ่มเติม

Lavender Benefits

Relaxation

Wellness

Relaxation

ช่วยให้ผ่อนคลาย

\ No newline at end of file diff --git a/package.json b/package.json index d621205..91da5ee 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "dev": "npm run dev --workspaces --if-present", "build": "npm run build --workspaces --if-present", "test": "npm run test --workspaces --if-present" + }, + "dependencies": { + "cheerio": "^1.2.0" } } diff --git a/packages/types/index.ts b/packages/types/index.ts index ea3c9c1..421db40 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -271,6 +271,7 @@ export const ProductsSectionPayloadSchema = z.object({ title: LocalizedStringSchema, subtitle: LocalizedStringSchema.optional(), type: z.enum(['featured', 'brand', 'custom']), + categoryId: z.number().optional(), brandId: z.number().optional(), productIds: z.array(z.number()).optional(), limit: z.number().optional(), @@ -354,46 +355,59 @@ export interface CustomerAddress { } -export const HeroArticlePayloadSchema = z.object({ - title: z.string().min(1, "Title is required"), - subtitle: z.string().optional(), - author: z.string().optional(), - publishDate: z.string().optional(), // Or z.date() if preferred, but string works for ISO dates - backgroundImageUrl: z.string().url("Must be a valid URL").optional(), +export const HeroArticleSectionPayloadSchema = z.object({ + title: LocalizedStringSchema, + subtitle: LocalizedStringSchema.optional(), + author: LocalizedStringSchema.optional(), + category: LocalizedStringSchema.optional(), + publishDate: z.string().optional(), + date: z.string().optional(), + imageUrl: z.string().optional(), + backgroundImageUrl: z.string().optional(), overlayColor: z.string().optional(), }); -export type HeroArticlePayload = z.infer; +export type HeroArticleSectionPayload = z.infer; -export const RichTextPayloadSchema = z.object({ - content: z.string().min(1, "Content is required"), +export const RichTextSectionPayloadSchema = z.object({ + title: LocalizedStringSchema.optional(), + content: LocalizedStringSchema, }); -export type RichTextPayload = z.infer; +export type RichTextSectionPayload = z.infer; -export const ScentNotesBreakdownPayloadSchema = z.object({ +export const ScentNotesBreakdownSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), description: LocalizedStringSchema.optional(), - topNotes: z.array(LocalizedStringSchema), - middleNotes: z.array(LocalizedStringSchema), - baseNotes: z.array(LocalizedStringSchema), + topNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(), + heartNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(), + middleNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(), + baseNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(), }); -export type ScentNotesBreakdownPayload = z.infer; +export type ScentNotesBreakdownSectionPayload = z.infer; -export const QuoteCalloutPayloadSchema = z.object({ - quote: z.string().min(1, "Quote is required"), - author: z.string().optional(), +export const QuoteCalloutSectionPayloadSchema = z.object({ + quote: LocalizedStringSchema, + author: LocalizedStringSchema.optional(), + role: LocalizedStringSchema.optional(), }); -export type QuoteCalloutPayload = z.infer; +export type QuoteCalloutSectionPayload = z.infer; -export const ProductCarouselPayloadSchema = z.object({ +export const ProductCarouselSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), description: LocalizedStringSchema.optional(), collectionId: z.string().optional(), - limit: z.number().int().positive().optional(), + limit: z.number().positive().optional(), showPrice: z.boolean().optional(), showAddToCart: z.boolean().optional(), products: z.array(z.any()).optional(), }); -export type ProductCarouselPayload = z.infer; +export type ProductCarouselSectionPayload = z.infer; + +// Aliases for backwards compatibility in ScentGuideSectionSchema +export const HeroArticlePayloadSchema = HeroArticleSectionPayloadSchema; +export const RichTextPayloadSchema = RichTextSectionPayloadSchema; +export const ScentNotesBreakdownPayloadSchema = ScentNotesBreakdownSectionPayloadSchema; +export const QuoteCalloutPayloadSchema = QuoteCalloutSectionPayloadSchema; +export const ProductCarouselPayloadSchema = ProductCarouselSectionPayloadSchema; export const ScentGuideSectionSchema = z.discriminatedUnion("type", [ z.object({ @@ -442,6 +456,7 @@ export const ScentGuidePageSchema = z.object({ export type ScentGuidePage = z.infer; export const AccordionSectionPayloadSchema = z.object({ + title: LocalizedStringSchema.optional(), categories: z.array( z.object({ name: LocalizedStringSchema, @@ -451,23 +466,24 @@ export const AccordionSectionPayloadSchema = z.object({ q: LocalizedStringSchema, a: LocalizedStringSchema, }) - ), + ).min(1), }) - ), + ).min(1), }); export type AccordionSectionPayload = z.infer; export const ContactInfoSectionPayloadSchema = z.object({ - address: z.string().min(1, "Address is required"), - email: z.string().min(1, "Email is required"), - line: z.string().min(1, "Line ID is required"), - hours: z.string().min(1, "Business hours are required"), + title: LocalizedStringSchema.optional(), + address: LocalizedStringSchema, + email: LocalizedStringSchema, + line: LocalizedStringSchema, + hours: LocalizedStringSchema, }); export type ContactInfoSectionPayload = z.infer; export const ContactFormSectionPayloadSchema = z.object({ - title: z.string().optional(), - subtitle: z.string().optional(), + title: LocalizedStringSchema.optional(), + subtitle: LocalizedStringSchema.optional(), }); export type ContactFormSectionPayload = z.infer; diff --git a/parse.js b/parse.js new file mode 100644 index 0000000..e6e9c20 --- /dev/null +++ b/parse.js @@ -0,0 +1,31 @@ +const cheerio = require('cheerio'); +const fs = require('fs'); +const html = fs.readFileSync('mega01.html', 'utf8'); +const $ = cheerio.load(html); + +console.log('--- Page Title ---'); +console.log($('title').text()); + +console.log('\n--- Main Content Sections ---'); +const sections = []; +$('section, .section, [data-section]').each((i, el) => { + sections.push({ + index: i + 1, + id: $(el).attr('id') || 'no-id', + class: $(el).attr('class') || 'no-class', + text: $(el).text().replace(/\s+/g, ' ').substring(0, 500) + }); +}); +if (sections.length === 0) { + // If no semantic tags, maybe just print divs with specific classes + $('div').each((i, el) => { + if($(el).attr('class') && $(el).attr('class').includes('py-')) { + sections.push({ + index: i + 1, + class: $(el).attr('class') || 'no-class', + text: $(el).text().replace(/\s+/g, ' ').substring(0, 500) + }); + } + }); +} +console.log(JSON.stringify(sections, null, 2)); diff --git a/update_pages.js b/update_pages.js new file mode 100644 index 0000000..fdb981c --- /dev/null +++ b/update_pages.js @@ -0,0 +1,89 @@ +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, 'apps/backend/src/routes/pages.ts'); +let content = fs.readFileSync(filePath, 'utf8'); + +content = content.replace(/import \{[\s\S]*?\} from "@homaroma\/types";/, `import { + LocalizedStringSchema, + HeroSectionPayloadSchema, + CategoriesSectionPayloadSchema, + ProductsSectionPayloadSchema, + BannerSectionPayloadSchema, + TextSectionPayloadSchema, + AnnouncementBarSectionPayloadSchema, + NewsletterSectionPayloadSchema, + EditorialTileSectionPayloadSchema, + EditorialCarouselSectionPayloadSchema, + HeroArticleSectionPayloadSchema, + RichTextSectionPayloadSchema, + ScentNotesBreakdownSectionPayloadSchema, + QuoteCalloutSectionPayloadSchema, + ProductCarouselSectionPayloadSchema, + AccordionSectionPayloadSchema, + ContactInfoSectionPayloadSchema, + ContactFormSectionPayloadSchema +} from "@homaroma/types"; +import { z } from "zod";`); + +// Remove manual validation functions +content = content.replace(/function isLocalizedStringValue[\s\S]*?function validateContactFormPayload[^{]*\{[\s\S]*?return null;\n\}/, ''); + +// Refactor validateSectionPayload +content = content.replace(/function validateSectionPayload\(type: string, payload: any\): string \| null \{[\s\S]*?return null;\n\}/, `function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } { + if (!payload || typeof payload !== 'object') { + return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] }; + } + + let schema: z.ZodTypeAny; + + switch (type.toUpperCase()) { + case 'HERO': schema = HeroSectionPayloadSchema; break; + case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break; + case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break; + case 'BANNER': schema = BannerSectionPayloadSchema; break; + case 'TEXT': schema = TextSectionPayloadSchema; break; + case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break; + case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break; + case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break; + case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break; + case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break; + case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break; + case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break; + case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break; + case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break; + case 'ACCORDION': schema = AccordionSectionPayloadSchema; break; + case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break; + case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break; + default: + return { success: false, issues: [{ code: 'custom', message: \`Unknown section type: \${type}\`, path: [] }] }; + } + + const res = schema.safeParse(payload); + if (!res.success) { + return { success: false, issues: res.error.issues }; + } + return { success: true }; +}`); + +// Update error handling in POST /:slug/sections +content = content.replace( + /const validationError = validateSectionPayload\(type, payload\);\n\s*if \(validationError\) \{\n\s*set\.status = 400;\n\s*return \{ error: validationError \};\n\s*\}/g, + `const validationResult = validateSectionPayload(type, payload); + if (!validationResult.success) { + set.status = 400; + return { error: "Validation failed", details: validationResult.issues }; + }` +); + +// Update error handling in PUT /sections/:sectionId +content = content.replace( + /const validationError = validateSectionPayload\(validationType, payload\);\n\s*if \(validationError\) \{\n\s*set\.status = 400;\n\s*return \{ error: validationError \};\n\s*\}/g, + `const validationResult = validateSectionPayload(validationType, payload); + if (!validationResult.success) { + set.status = 400; + return { error: "Validation failed", details: validationResult.issues }; + }` +); + +fs.writeFileSync(filePath, content);