x
This commit is contained in:
1 parent
c1c3a43d7d
commit
a5f6e3171b
11 files changed
+351
-13
No files matched your search
@@ -12,6 +12,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@homaroma/types": "*",
|
||||||
"@elysiajs/cors": "^1.2.2",
|
"@elysiajs/cors": "^1.2.2",
|
||||||
"@elysiajs/jwt": "^1.4.1",
|
"@elysiajs/jwt": "^1.4.1",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
|
import {
|
||||||
|
HeroArticlePayloadSchema,
|
||||||
|
RichTextPayloadSchema,
|
||||||
|
ScentNotesBreakdownPayloadSchema,
|
||||||
|
QuoteCalloutPayloadSchema,
|
||||||
|
ProductCarouselPayloadSchema
|
||||||
|
} from "@homaroma/types";
|
||||||
|
|
||||||
export const pagesRoute = new Elysia({ prefix: "/pages" })
|
export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||||
.get("/", async () => {
|
.get("/", async () => {
|
||||||
@@ -113,15 +120,22 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
|||||||
.post("/", async ({ body, set }) => {
|
.post("/", async ({ body, set }) => {
|
||||||
try {
|
try {
|
||||||
const data = body as any;
|
const data = body as any;
|
||||||
const { id, slug, title, isPublished } = data;
|
const { id, slug, title, isPublished, status, metadata } = data;
|
||||||
|
let finalIsPublished = true;
|
||||||
|
if (status !== undefined) {
|
||||||
|
finalIsPublished = status === "published";
|
||||||
|
} else if (isPublished !== undefined) {
|
||||||
|
finalIsPublished = isPublished;
|
||||||
|
}
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
return await prisma.page.update({
|
return await prisma.page.update({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
data: { slug, title, isPublished: isPublished !== false }
|
data: { slug, title, isPublished: finalIsPublished }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await prisma.page.create({
|
return await prisma.page.create({
|
||||||
data: { slug, title, isPublished: isPublished !== false }
|
data: { slug, title, isPublished: finalIsPublished }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -134,7 +148,9 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
|||||||
id: t.Optional(t.Number()),
|
id: t.Optional(t.Number()),
|
||||||
slug: t.String(),
|
slug: t.String(),
|
||||||
title: t.String(),
|
title: t.String(),
|
||||||
isPublished: t.Optional(t.Boolean())
|
isPublished: t.Optional(t.Boolean()),
|
||||||
|
status: t.Optional(t.String()),
|
||||||
|
metadata: t.Optional(t.Any())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.delete("/:slug", async ({ params: { slug }, set }) => {
|
.delete("/:slug", async ({ params: { slug }, set }) => {
|
||||||
@@ -170,11 +186,20 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
|||||||
return { error: validationError };
|
return { error: validationError };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finalOrder = order;
|
||||||
|
if (finalOrder === undefined) {
|
||||||
|
const lastSection = await prisma.pageSection.findFirst({
|
||||||
|
where: { pageId },
|
||||||
|
orderBy: { order: "desc" }
|
||||||
|
});
|
||||||
|
finalOrder = lastSection ? lastSection.order + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.pageSection.create({
|
return await prisma.pageSection.create({
|
||||||
data: {
|
data: {
|
||||||
pageId,
|
pageId,
|
||||||
type: type.toUpperCase(),
|
type: type.toUpperCase(),
|
||||||
order,
|
order: finalOrder,
|
||||||
payload,
|
payload,
|
||||||
isActive: isActive !== false
|
isActive: isActive !== false
|
||||||
}
|
}
|
||||||
@@ -187,7 +212,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
|||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
type: t.String(),
|
type: t.String(),
|
||||||
order: t.Number(),
|
order: t.Optional(t.Number()),
|
||||||
payload: t.Any(),
|
payload: t.Any(),
|
||||||
isActive: t.Optional(t.Boolean())
|
isActive: t.Optional(t.Boolean())
|
||||||
})
|
})
|
||||||
@@ -373,6 +398,31 @@ function validateSectionPayload(type: string, payload: any): string | null {
|
|||||||
if (typeof slide.category !== 'string') return `Slide #${idx+1} must contain a category string`;
|
if (typeof slide.category !== 'string') return `Slide #${idx+1} must contain a category string`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'HERO_ARTICLE': {
|
||||||
|
const res = HeroArticlePayloadSchema.safeParse(payload);
|
||||||
|
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'RICH_TEXT': {
|
||||||
|
const res = RichTextPayloadSchema.safeParse(payload);
|
||||||
|
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SCENT_NOTES_BREAKDOWN': {
|
||||||
|
const res = ScentNotesBreakdownPayloadSchema.safeParse(payload);
|
||||||
|
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'QUOTE_CALLOUT': {
|
||||||
|
const res = QuoteCalloutPayloadSchema.safeParse(payload);
|
||||||
|
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'PRODUCT_CAROUSEL': {
|
||||||
|
const res = ProductCarouselPayloadSchema.safeParse(payload);
|
||||||
|
if (!res.success) return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return `Unknown section type: ${type}`;
|
return `Unknown section type: ${type}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
title: { type: "string", description: "Title of the page" },
|
title: { type: "string", description: "Title of the page" },
|
||||||
slug: { type: "string", description: "Unique slug identifier (e.g. about-us)" },
|
slug: { type: "string", description: "Unique slug identifier (e.g. about-us)" },
|
||||||
isPublished: { type: "boolean", description: "Publish status" },
|
isPublished: { type: "boolean", description: "Publish status" },
|
||||||
|
status: { type: "string", description: "Publish status (e.g. 'published' or 'draft')" },
|
||||||
|
metadata: { type: "object", description: "SEO metadata" },
|
||||||
id: { type: "number", description: "Numeric page ID (if updating existing page)" }
|
id: { type: "number", description: "Numeric page ID (if updating existing page)" }
|
||||||
},
|
},
|
||||||
required: ["title", "slug"]
|
required: ["title", "slug"]
|
||||||
@@ -225,12 +227,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
pageId: { type: "number", description: "The numeric ID of the page" },
|
pageId: { type: "number", description: "The numeric ID of the page" },
|
||||||
type: { type: "string", description: "Block type (e.g. HERO, ANNOUNCEMENT_BAR, NEWSLETTER, EDITORIAL_TILE, EDITORIAL_CAROUSEL, PRODUCTS, CATEGORIES, TEXT, BANNER)" },
|
type: { type: "string", description: "Block type (e.g. HERO, ANNOUNCEMENT_BAR, NEWSLETTER, EDITORIAL_TILE, EDITORIAL_CAROUSEL, PRODUCTS, CATEGORIES, TEXT, BANNER, HERO_ARTICLE, RICH_TEXT, SCENT_NOTES_BREAKDOWN, QUOTE_CALLOUT, PRODUCT_CAROUSEL)" },
|
||||||
order: { type: "number", description: "Display order sequence (starts at 1)" },
|
order: { type: "number", description: "Display order sequence (starts at 1)" },
|
||||||
payload: { type: "object", description: "The JSON payload matching the section type rules" },
|
payload: { type: "object", description: "The JSON payload matching the section type rules" },
|
||||||
isActive: { type: "boolean", description: "Initial active state" }
|
isActive: { type: "boolean", description: "Initial active state" }
|
||||||
},
|
},
|
||||||
required: ["pageId", "type", "order", "payload"]
|
required: ["pageId", "type", "payload"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,8 +388,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
const payload: any = {
|
const payload: any = {
|
||||||
title: params.title,
|
title: params.title,
|
||||||
slug: params.slug,
|
slug: params.slug,
|
||||||
isPublished: params.isPublished !== false
|
|
||||||
};
|
};
|
||||||
|
if (params.isPublished !== undefined) payload.isPublished = params.isPublished;
|
||||||
|
if (params.status !== undefined) payload.status = params.status;
|
||||||
|
if (params.metadata !== undefined) payload.metadata = params.metadata;
|
||||||
if (params.id) {
|
if (params.id) {
|
||||||
payload.id = params.id;
|
payload.id = params.id;
|
||||||
}
|
}
|
||||||
@@ -395,12 +399,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||||
}
|
}
|
||||||
case "add_page_section": {
|
case "add_page_section": {
|
||||||
const payload = {
|
const payload: any = {
|
||||||
type: params.type,
|
type: params.type,
|
||||||
order: params.order,
|
|
||||||
payload: params.payload,
|
payload: params.payload,
|
||||||
isActive: params.isActive !== false
|
isActive: params.isActive !== false
|
||||||
};
|
};
|
||||||
|
if (params.order !== undefined) payload.order = params.order;
|
||||||
const res = await api.post(`/pages/${params.pageId}/sections`, payload);
|
const res = await api.post(`/pages/${params.pageId}/sections`, payload);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DynamicHeroArticleProps {
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DynamicHeroArticle({ payload }: DynamicHeroArticleProps) {
|
||||||
|
const { title, subtitle, imageUrl, author, date, category } = payload || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative w-full min-h-[60vh] md:min-h-[80vh] flex items-center justify-center bg-spa-sage overflow-hidden">
|
||||||
|
{imageUrl && (
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<img src={imageUrl} alt={title || 'Article Hero'} className="w-full h-full object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-black/40" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative z-10 text-center max-w-4xl px-6 text-white">
|
||||||
|
{category && (
|
||||||
|
<span className="text-sm uppercase tracking-widest font-bold mb-4 block text-spa-gold">
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h1 className="text-4xl md:text-6xl font-serif mb-6 leading-tight">
|
||||||
|
{title || 'Scent Guide Article'}
|
||||||
|
</h1>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-lg md:text-xl font-light opacity-90 max-w-2xl mx-auto mb-8">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{(author || date) && (
|
||||||
|
<div className="text-sm font-medium tracking-wide uppercase opacity-80 flex items-center justify-center gap-4">
|
||||||
|
{author && <span>By {author}</span>}
|
||||||
|
{author && date && <span>•</span>}
|
||||||
|
{date && <span>{date}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ import DynamicAnnouncementBar from './DynamicAnnouncementBar';
|
|||||||
import DynamicNewsletter from './DynamicNewsletter';
|
import DynamicNewsletter from './DynamicNewsletter';
|
||||||
import DynamicEditorialTile from './DynamicEditorialTile';
|
import DynamicEditorialTile from './DynamicEditorialTile';
|
||||||
import DynamicEditorialCarousel from './DynamicEditorialCarousel';
|
import DynamicEditorialCarousel from './DynamicEditorialCarousel';
|
||||||
|
import DynamicHeroArticle from './DynamicHeroArticle';
|
||||||
|
import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown';
|
||||||
|
import DynamicQuoteCallout from './DynamicQuoteCallout';
|
||||||
|
import DynamicProductCarousel from './DynamicProductCarousel';
|
||||||
|
|
||||||
interface DynamicPageRendererProps {
|
interface DynamicPageRendererProps {
|
||||||
sections: PageSection[];
|
sections: PageSection[];
|
||||||
@@ -51,6 +55,14 @@ export default function DynamicPageRenderer({
|
|||||||
return <DynamicEditorialTile key={section.id} payload={section.payload} />;
|
return <DynamicEditorialTile key={section.id} payload={section.payload} />;
|
||||||
case 'EDITORIAL_CAROUSEL':
|
case 'EDITORIAL_CAROUSEL':
|
||||||
return <DynamicEditorialCarousel key={section.id} payload={section.payload} />;
|
return <DynamicEditorialCarousel key={section.id} payload={section.payload} />;
|
||||||
|
case 'HERO_ARTICLE':
|
||||||
|
return <DynamicHeroArticle key={section.id} payload={section.payload} />;
|
||||||
|
case 'SCENT_NOTES_BREAKDOWN':
|
||||||
|
return <DynamicScentNotesBreakdown key={section.id} payload={section.payload} />;
|
||||||
|
case 'QUOTE_CALLOUT':
|
||||||
|
return <DynamicQuoteCallout key={section.id} payload={section.payload} />;
|
||||||
|
case 'PRODUCT_CAROUSEL':
|
||||||
|
return <DynamicProductCarousel key={section.id} payload={section.payload} />;
|
||||||
default:
|
default:
|
||||||
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
|
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
|
||||||
|
import ProductCard from './ProductCard';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/pagination';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
|
interface DynamicProductCarouselProps {
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DynamicProductCarousel({ payload }: DynamicProductCarouselProps) {
|
||||||
|
const { title, description, products = [] } = payload || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-spa-sage/30 px-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{(title || description) && (
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
{title && <h2 className="text-3xl md:text-5xl font-serif text-spa-forest mb-4">{title}</h2>}
|
||||||
|
{description && <p className="text-lg text-spa-stone/70 max-w-2xl mx-auto">{description}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{products.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-spa-stone/50 italic">
|
||||||
|
No products to display.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative pb-12">
|
||||||
|
<Swiper
|
||||||
|
modules={[Autoplay, Pagination, Navigation]}
|
||||||
|
spaceBetween={32}
|
||||||
|
slidesPerView={1}
|
||||||
|
breakpoints={{
|
||||||
|
640: { slidesPerView: 2 },
|
||||||
|
1024: { slidesPerView: 3 },
|
||||||
|
1280: { slidesPerView: 4 }
|
||||||
|
}}
|
||||||
|
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-spa-forest' }}
|
||||||
|
navigation
|
||||||
|
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||||
|
className="w-full !px-4"
|
||||||
|
>
|
||||||
|
{products.map((p: any) => (
|
||||||
|
<SwiperSlide key={p.id} className="h-auto">
|
||||||
|
<ProductCard product={p} enableQuickShop={true} />
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DynamicQuoteCalloutProps {
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DynamicQuoteCallout({ payload }: DynamicQuoteCalloutProps) {
|
||||||
|
const { quote, author, role } = payload || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 px-6 bg-white">
|
||||||
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
|
<svg className="w-12 h-12 mx-auto text-spa-gold/40 mb-8" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
|
||||||
|
</svg>
|
||||||
|
<blockquote className="text-2xl md:text-4xl font-serif text-spa-forest italic leading-relaxed mb-8">
|
||||||
|
"{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}"
|
||||||
|
</blockquote>
|
||||||
|
{(author || role) && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
{author && <span className="font-bold uppercase tracking-widest text-spa-stone">{author}</span>}
|
||||||
|
{role && <span className="text-sm text-spa-stone/60 mt-1 uppercase tracking-wide">{role}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface DynamicScentNotesBreakdownProps {
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNotesBreakdownProps) {
|
||||||
|
const { title, description, topNotes, heartNotes, baseNotes } = payload || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6 bg-spa-light">
|
||||||
|
<div className="max-w-5xl mx-auto text-center">
|
||||||
|
<h2 className="text-3xl md:text-5xl font-serif text-spa-forest mb-6">
|
||||||
|
{title || 'Scent Notes'}
|
||||||
|
</h2>
|
||||||
|
{description && (
|
||||||
|
<p className="text-lg text-spa-stone/80 max-w-2xl mx-auto mb-16">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||||
|
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Top Notes</h3>
|
||||||
|
<p className="text-spa-stone font-light leading-relaxed">
|
||||||
|
{topNotes || 'The initial, lighter smells of the fragrance.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||||
|
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Heart Notes</h3>
|
||||||
|
<p className="text-spa-stone font-light leading-relaxed">
|
||||||
|
{heartNotes || 'The core of the fragrance, emerging just after the top notes.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8 bg-white rounded-2xl shadow-sm border border-spa-stone/10">
|
||||||
|
<h3 className="text-xl font-bold uppercase tracking-widest text-spa-forest mb-4">Base Notes</h3>
|
||||||
|
<p className="text-spa-stone font-light leading-relaxed">
|
||||||
|
{baseNotes || 'The deepest, richest scents that linger the longest.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"@elysiajs/jwt": "^1.4.1",
|
"@elysiajs/jwt": "^1.4.1",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
"@elysiajs/swagger": "^1.2.1",
|
"@elysiajs/swagger": "^1.2.1",
|
||||||
|
"@homaroma/types": "*",
|
||||||
"@prisma/adapter-pg": "^7.6.0",
|
"@prisma/adapter-pg": "^7.6.0",
|
||||||
"@prisma/client": "^7.6.0",
|
"@prisma/client": "^7.6.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@@ -137,6 +138,9 @@
|
|||||||
"packages/types": {
|
"packages/types": {
|
||||||
"name": "@homaroma/types",
|
"name": "@homaroma/types",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.4.3",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
@@ -1640,7 +1644,7 @@
|
|||||||
|
|
||||||
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
@@ -1654,6 +1658,8 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@prisma/dev/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"@prisma/dev/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
||||||
@@ -1704,6 +1710,8 @@
|
|||||||
|
|
||||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
"express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
@@ -1754,6 +1762,8 @@
|
|||||||
|
|
||||||
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
|
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
|
||||||
|
|
||||||
|
"@scalar/themes/@scalar/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|||||||
@@ -277,4 +277,88 @@ export interface CustomerAddress {
|
|||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
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(),
|
||||||
|
overlayColor: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type HeroArticlePayload = z.infer<typeof HeroArticlePayloadSchema>;
|
||||||
|
|
||||||
|
export const RichTextPayloadSchema = z.object({
|
||||||
|
content: z.string().min(1, "Content is required"),
|
||||||
|
});
|
||||||
|
export type RichTextPayload = z.infer<typeof RichTextPayloadSchema>;
|
||||||
|
|
||||||
|
export const ScentNotesBreakdownPayloadSchema = z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
topNotes: z.array(z.string()),
|
||||||
|
middleNotes: z.array(z.string()),
|
||||||
|
baseNotes: z.array(z.string()),
|
||||||
|
});
|
||||||
|
export type ScentNotesBreakdownPayload = z.infer<typeof ScentNotesBreakdownPayloadSchema>;
|
||||||
|
|
||||||
|
export const QuoteCalloutPayloadSchema = z.object({
|
||||||
|
quote: z.string().min(1, "Quote is required"),
|
||||||
|
author: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type QuoteCalloutPayload = z.infer<typeof QuoteCalloutPayloadSchema>;
|
||||||
|
|
||||||
|
export const ProductCarouselPayloadSchema = z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
collectionId: z.string().optional(),
|
||||||
|
limit: z.number().int().positive().optional(),
|
||||||
|
showPrice: z.boolean().optional(),
|
||||||
|
showAddToCart: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
export type ProductCarouselPayload = z.infer<typeof ProductCarouselPayloadSchema>;
|
||||||
|
|
||||||
|
export const ScentGuideSectionSchema = z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
id: z.string().or(z.number()),
|
||||||
|
type: z.literal("hero_article"),
|
||||||
|
order: z.number(),
|
||||||
|
payload: HeroArticlePayloadSchema,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
id: z.string().or(z.number()),
|
||||||
|
type: z.literal("rich_text"),
|
||||||
|
order: z.number(),
|
||||||
|
payload: RichTextPayloadSchema,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
id: z.string().or(z.number()),
|
||||||
|
type: z.literal("scent_notes_breakdown"),
|
||||||
|
order: z.number(),
|
||||||
|
payload: ScentNotesBreakdownPayloadSchema,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
id: z.string().or(z.number()),
|
||||||
|
type: z.literal("quote_callout"),
|
||||||
|
order: z.number(),
|
||||||
|
payload: QuoteCalloutPayloadSchema,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
id: z.string().or(z.number()),
|
||||||
|
type: z.literal("product_carousel"),
|
||||||
|
order: z.number(),
|
||||||
|
payload: ProductCarouselPayloadSchema,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ScentGuidePageSchema = z.object({
|
||||||
|
pageId: z.string().or(z.number()),
|
||||||
|
title: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
status: z.string().optional(),
|
||||||
|
metadata: z.object({
|
||||||
|
seoTitle: z.string().optional(),
|
||||||
|
seoDescription: z.string().optional(),
|
||||||
|
}).optional(),
|
||||||
|
sections: z.array(ScentGuideSectionSchema),
|
||||||
|
});
|
||||||
|
export type ScentGuidePage = z.infer<typeof ScentGuidePageSchema>;
|
||||||
@@ -3,5 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"types": "index.ts"
|
"types": "index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.4.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user