x
This commit is contained in:
1 parent
d325cd8763
commit
c6bf2f70ac
45 files changed
+5195
-338
No files matched your search
@@ -0,0 +1,61 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Customer" ADD COLUMN "points" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "pointsDiscount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "pointsEarned" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "pointsUsed" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "promoLevel1Disc" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "promoLevel1Name" TEXT,
|
||||
ADD COLUMN "promoLevel2Disc" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "promoLevel2Name" TEXT,
|
||||
ADD COLUMN "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "trackingNo" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "StoreSettings" ADD COLUMN "pointsEarnRate" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
||||
ALTER COLUMN "bankAccountName" SET DEFAULT 'HomAroma Co., Ltd.';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Promotion" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"level" INTEGER NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"minPurchase" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"startDate" TIMESTAMP(3),
|
||||
"endDate" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Promotion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Review" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"rating" INTEGER NOT NULL DEFAULT 5,
|
||||
"comment" TEXT,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"customerId" INTEGER,
|
||||
"customerName" TEXT,
|
||||
"isPublished" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Review_productId_idx" ON "Review"("productId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Review_customerId_idx" ON "Review"("customerId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Review" ADD CONSTRAINT "Review_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Page" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"isPublished" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Page_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PageSection" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"pageId" INTEGER NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"order" INTEGER NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PageSection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Page_slug_key" ON "Page"("slug");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PageSection" ADD CONSTRAINT "PageSection_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "NewsletterSubscription" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "NewsletterSubscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "NewsletterSubscription_email_key" ON "NewsletterSubscription"("email");
|
||||
@@ -165,6 +165,7 @@ model Order {
|
||||
totalAmount Float
|
||||
status String @default("PENDING_PAYMENT")
|
||||
paymentSlipUrl String?
|
||||
trackingNo String?
|
||||
|
||||
customerId Int?
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
@@ -420,3 +421,33 @@ model Review {
|
||||
@@index([productId])
|
||||
@@index([customerId])
|
||||
}
|
||||
|
||||
model Page {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique // e.g., "home", "about-us"
|
||||
title String
|
||||
sections PageSection[]
|
||||
isPublished Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model PageSection {
|
||||
id Int @id @default(autoincrement())
|
||||
pageId Int
|
||||
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
|
||||
type String // e.g., "HERO", "CATEGORIES", "PRODUCTS", "BANNER", "TEXT"
|
||||
order Int // Display order
|
||||
payload Json // Dynamic JSON payload
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model NewsletterSubscription {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Pool } from 'pg';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
console.log("🌱 Seeding Dynamic Page Builder...");
|
||||
|
||||
// 1. Create the HomePage
|
||||
const page = await prisma.page.upsert({
|
||||
where: { slug: "home" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "home",
|
||||
title: "Home Page",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${page.title} (slug: ${page.slug})`);
|
||||
|
||||
// Clear existing sections for home page so we don't duplicate
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: page.id }
|
||||
});
|
||||
|
||||
// 2. Create default sections: HERO, CATEGORIES, PRODUCTS, BANNER, TEXT
|
||||
|
||||
// Section 1: Hero
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: page.id,
|
||||
type: "HERO",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
tagline: "VIBRANT ELEGANCE COLLECTION",
|
||||
title: "Inhale [Serenity]",
|
||||
description: "Crafting premium sensory experiences that transform your space into a sanctuary of peace.",
|
||||
imageUrl: "/images/hero.png",
|
||||
buttonText: "Explore Collection",
|
||||
buttonLink: "/shop"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Section 2: Categories
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: page.id,
|
||||
type: "CATEGORIES",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Our Collections",
|
||||
subtitle: "Discover the perfect scent for every moment"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Section 3: Products (Featured Picks)
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: page.id,
|
||||
type: "PRODUCTS",
|
||||
order: 3,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Featured Picks",
|
||||
subtitle: "Hand-picked fragrances recommended just for you",
|
||||
type: "featured",
|
||||
limit: 3
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Section 4: Banner
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: page.id,
|
||||
type: "BANNER",
|
||||
order: 4,
|
||||
isActive: true,
|
||||
payload: {
|
||||
imageUrl: "/images/oil_ozone.png",
|
||||
linkUrl: "/category/1",
|
||||
title: "Special Offer",
|
||||
description: "Get 10% off on your first purchase of Oil Ozone collection",
|
||||
buttonText: "Shop Now"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Section 5: Text info
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: page.id,
|
||||
type: "TEXT",
|
||||
order: 5,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "About HomAroma",
|
||||
content: "HomAroma is dedicated to sourcing the finest raw materials to formulate high-quality essential oils, reed diffusers, and room sprays. Our goal is to bring nature's purity directly into your life.",
|
||||
textAlign: "center"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Seeded 5 default home page sections successfully!");
|
||||
|
||||
// 3. Create the Shop Page Layout
|
||||
const shopPage = await prisma.page.upsert({
|
||||
where: { slug: "shop-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "shop-layout",
|
||||
title: "Shop Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${shopPage.title} (slug: ${shopPage.slug})`);
|
||||
|
||||
// Clear existing sections for shop page so we don't duplicate
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: shopPage.id }
|
||||
});
|
||||
|
||||
// Shop Section 1: Announcement Bar
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: shopPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["[FREE SHIPPING] on orders over ฿1,500", "New Premium Collection is out now!"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Shop Section 2: Hero Banner (Set to active by default to showcase the header capabilities)
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: shopPage.id,
|
||||
type: "HERO",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
tagline: "CURATED BOTANICALS",
|
||||
title: "The [Shop] Collection",
|
||||
description: "Transform your daily space into a luxury escape with our natural aroma range.",
|
||||
imageUrl: "/images/hero.png",
|
||||
buttonText: "Learn More",
|
||||
buttonLink: "/about"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Shop Section 3: Editorial Tile
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: shopPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 3,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Crafted with [Sensory Passion]",
|
||||
description: "Every formulation at HomAroma is a tribute to pure botanical essences, hand-blended to capture nature’s quiet intelligence.",
|
||||
imageUrl: "/images/oil_ozone.png",
|
||||
imagePosition: "right",
|
||||
buttonText: "Read Our Story",
|
||||
buttonLink: "/about",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Shop Section 4: Newsletter
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: shopPage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 4,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Begin Your Wellness Journey",
|
||||
description: "Subscribe to receive 10% off your first purchase and exclusive updates.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Seeded default shop-layout page sections successfully!");
|
||||
|
||||
// 4. Create the Scent Guide Page Layout
|
||||
const scentGuidePage = await prisma.page.upsert({
|
||||
where: { slug: "scent-guide-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "scent-guide-layout",
|
||||
title: "Scent Guide Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${scentGuidePage.title} (slug: ${scentGuidePage.slug})`);
|
||||
|
||||
// Clear existing sections for scent guide page so we don't duplicate
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: scentGuidePage.id }
|
||||
});
|
||||
|
||||
// Scent Guide Section 1: Announcement Bar
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: scentGuidePage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Explore our Scent Guide to find your signature fragrance", "Read wellness articles crafted by experts"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Scent Guide Section 2: Hero Banner
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: scentGuidePage.id,
|
||||
type: "HERO",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
tagline: "JOURNAL & INSIGHTS",
|
||||
title: "The [Scent] Guide",
|
||||
description: "Welcome to the sensory archive of HomAroma. Explore the art and science of botanical aromatherapy.",
|
||||
imageUrl: "/images/hero.png",
|
||||
buttonText: "Read Story",
|
||||
buttonLink: "/about"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Scent Guide Section 3: Newsletter
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: scentGuidePage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 3,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Stay Inspired",
|
||||
description: "Subscribe to our journal and get regular wellness guides, blending tips, and exclusive offers.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Seeded default scent-guide-layout page sections successfully!");
|
||||
|
||||
// 5. Create Category Page Layout
|
||||
const categoryPage = await prisma.page.upsert({
|
||||
where: { slug: "category-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "category-layout",
|
||||
title: "Category Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${categoryPage.title} (slug: ${categoryPage.slug})`);
|
||||
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: categoryPage.id }
|
||||
});
|
||||
|
||||
// Announcement Bar for Categories
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: categoryPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Explore our specialized collections for your sanctuary", "Free premium shipping on orders over ฿1,500"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Newsletter at the bottom of Categories
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: categoryPage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Join [HomAroma] Circle",
|
||||
description: "Subscribe to receive announcements of new single-origin botanical releases.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 6. Create Product Detail Page Layout
|
||||
const productDetailPage = await prisma.page.upsert({
|
||||
where: { slug: "product-detail-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "product-detail-layout",
|
||||
title: "Product Detail Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${productDetailPage.title} (slug: ${productDetailPage.slug})`);
|
||||
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: productDetailPage.id }
|
||||
});
|
||||
|
||||
// Announcement Bar for Product Details
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: productDetailPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Special Offer: Get 10% off your first purchase of single-origin oils", "Hand-formulated with pure botanical extracts"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Editorial banner at the bottom of Product Details
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: productDetailPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "The [Botanical] Sourcing",
|
||||
description: "All our raw materials are sustainably harvested at peak potency from verified organic micro-farms worldwide.",
|
||||
imageUrl: "/images/oil_ozone.png",
|
||||
imagePosition: "left",
|
||||
buttonText: "Sourcing Map",
|
||||
buttonLink: "/about",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Create FAQ Page Layout
|
||||
const faqPage = await prisma.page.upsert({
|
||||
where: { slug: "faq-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "faq-layout",
|
||||
title: "FAQ Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Created page: ${faqPage.title} (slug: ${faqPage.slug})`);
|
||||
|
||||
await prisma.pageSection.deleteMany({
|
||||
where: { pageId: faqPage.id }
|
||||
});
|
||||
|
||||
// Announcement Bar for FAQ
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: faqPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Need instant help? Reach out on LINE @homaroma", "FAQ updated for Summer 2026 delivery guidelines"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 4000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Newsletter at the bottom of FAQ
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: faqPage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Stay informed with [HomAroma]",
|
||||
description: "Sign up to receive shipping updates, care guides, and exclusive collection invites.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Seeded default category, product-detail, and FAQ layout sections successfully!");
|
||||
|
||||
// 8. Create Wishlist Page Layout
|
||||
const wishlistPage = await prisma.page.upsert({
|
||||
where: { slug: "wishlist-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "wishlist-layout",
|
||||
title: "Wishlist Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${wishlistPage.title} (slug: ${wishlistPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: wishlistPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: wishlistPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Sign up to HomAroma Circle to get 10% off your wishlist items", "Hand-crafted botanicals shipped directly to you"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: wishlistPage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Don't lose your favorites",
|
||||
description: "Subscribe to receive price drops, back-in-stock alerts, and exclusive discounts on items in your wishlist.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 9. Create Search Page Layout
|
||||
const searchPage = await prisma.page.upsert({
|
||||
where: { slug: "search-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "search-layout",
|
||||
title: "Search Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${searchPage.title} (slug: ${searchPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: searchPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: searchPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Need help finding something? Live Chat with our scent experts on LINE @homaroma"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: searchPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Find Your [Signature Scent]",
|
||||
description: "Aromatherapy is the art of tuning your environment. Discover our curation of room sprays, reed diffusers, and essential oils designed for modern sanctuaries.",
|
||||
imageUrl: "/images/hero.png",
|
||||
imagePosition: "right",
|
||||
buttonText: "Explore Guide",
|
||||
buttonLink: "/scent-guide",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 10. Create Coupons Page Layout
|
||||
const couponsPage = await prisma.page.upsert({
|
||||
where: { slug: "coupons-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "coupons-layout",
|
||||
title: "Coupons Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${couponsPage.title} (slug: ${couponsPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: couponsPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: couponsPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Enjoy limited-time discount codes for your sensory sanctuary"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: couponsPage.id,
|
||||
type: "NEWSLETTER",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Get Exclusive Secret Codes",
|
||||
description: "Become a subscriber to get weekly private discount codes and early access to new collections.",
|
||||
buttonText: "Subscribe",
|
||||
placeholderText: "Enter your email address",
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "I agree to the privacy policy."
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 11. Create Contact Page Layout
|
||||
const contactPage = await prisma.page.upsert({
|
||||
where: { slug: "contact-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "contact-layout",
|
||||
title: "Contact Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${contactPage.title} (slug: ${contactPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: contactPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: contactPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Customer support is available Mon-Sat, 9:00 - 18:00 on LINE @homaroma"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 4000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: contactPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Our [Bangkok Studio]",
|
||||
description: "Visit our flagship boutique to experience custom blending sessions and smell our full botanical library in person.",
|
||||
imageUrl: "/images/hero.png",
|
||||
imagePosition: "left",
|
||||
buttonText: "Book Session",
|
||||
buttonLink: "/about",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 12. Create Order Success Page Layout
|
||||
const orderSuccessPage = await prisma.page.upsert({
|
||||
where: { slug: "order-success-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "order-success-layout",
|
||||
title: "Order Success Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${orderSuccessPage.title} (slug: ${orderSuccessPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: orderSuccessPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: orderSuccessPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Thank you for shopping with HomAroma! Your package is preparing.", "Your order tracking status will be updated here."],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: orderSuccessPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Crafting Your [Sanctuary]",
|
||||
description: "Every order is carefully packaged by hand using sustainable, biodegradable materials. We ensure your botanicals arrive fresh and ready to enrich your space.",
|
||||
imageUrl: "/images/oil_ozone.png",
|
||||
imagePosition: "right",
|
||||
buttonText: "Our Sustainability Commitments",
|
||||
buttonLink: "/about",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 13. Create Track Order Page Layout
|
||||
const trackOrderPage = await prisma.page.upsert({
|
||||
where: { slug: "track-order-layout" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "track-order-layout",
|
||||
title: "Track Order Page Layout",
|
||||
isPublished: true,
|
||||
}
|
||||
});
|
||||
console.log(`✅ Created page: ${trackOrderPage.title} (slug: ${trackOrderPage.slug})`);
|
||||
await prisma.pageSection.deleteMany({ where: { pageId: trackOrderPage.id } });
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: trackOrderPage.id,
|
||||
type: "ANNOUNCEMENT_BAR",
|
||||
order: 1,
|
||||
isActive: true,
|
||||
payload: {
|
||||
slides: ["Check shipping status live. For any delivery queries, reach LINE @homaroma"],
|
||||
backgroundColor: "#1F3D2D",
|
||||
textColor: "#FDFDF9",
|
||||
speed: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId: trackOrderPage.id,
|
||||
type: "EDITORIAL_TILE",
|
||||
order: 2,
|
||||
isActive: true,
|
||||
payload: {
|
||||
title: "Scenting Your [Daily Rituals]",
|
||||
description: "While waiting for your order, explore how different botanical notes impact focus, sleep, and emotional balance in our Scent Guide.",
|
||||
imageUrl: "/images/hero.png",
|
||||
imagePosition: "left",
|
||||
buttonText: "Read Scent Guide",
|
||||
buttonLink: "/scent-guide",
|
||||
backgroundColor: "#FDFDF9",
|
||||
textColor: "#1F3D2D"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Seeded default Category, Product Detail, FAQ, Wishlist, Search, Coupons, Contact, Order Success, and Track Order layouts successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("❌ Error seeding page:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import { customersRoute } from "./routes/customers";
|
||||
import { couponsRoute } from "./routes/coupons";
|
||||
import { reviewsRoute } from "./routes/reviews";
|
||||
import { promotionsRoute } from "./routes/promotions";
|
||||
import { pagesRoute } from "./routes/pages";
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent";
|
||||
@@ -83,7 +84,7 @@ const app = new Elysia()
|
||||
return file;
|
||||
})
|
||||
.group("/api", (app) =>
|
||||
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute)
|
||||
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute)
|
||||
)
|
||||
.listen(PORT);
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
export const customersRoute = new Elysia({ prefix: "/customers" })
|
||||
.get("/", async () => {
|
||||
.get("/", async ({ query }) => {
|
||||
const search = query.search as string | undefined;
|
||||
|
||||
const whereClause = search ? {
|
||||
OR: [
|
||||
{ displayName: { contains: search, mode: "insensitive" as any } },
|
||||
{ email: { contains: search, mode: "insensitive" as any } },
|
||||
{ phone: { contains: search, mode: "insensitive" as any } },
|
||||
{ lineUserId: { contains: search, mode: "insensitive" as any } }
|
||||
]
|
||||
} : {};
|
||||
|
||||
return await prisma.customer.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
_count: {
|
||||
@@ -22,4 +34,36 @@ export const customersRoute = new Elysia({ prefix: "/customers" })
|
||||
return { error: "Customer not found" };
|
||||
}
|
||||
return customer;
|
||||
})
|
||||
.patch("/:id/points", async ({ params: { id }, body, set }) => {
|
||||
try {
|
||||
const { pointsDelta, reason } = body as { pointsDelta: number, reason?: string };
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
set.status = 404;
|
||||
return { error: "Customer not found" };
|
||||
}
|
||||
|
||||
const newPoints = Math.max(0, customer.points + pointsDelta);
|
||||
|
||||
const updatedCustomer = await prisma.customer.update({
|
||||
where: { id: Number(id) },
|
||||
data: { points: newPoints }
|
||||
});
|
||||
|
||||
return updatedCustomer;
|
||||
} catch (err) {
|
||||
console.error("[Customers] Adjust Points Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to adjust points" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
pointsDelta: t.Number(),
|
||||
reason: t.Optional(t.String())
|
||||
})
|
||||
});
|
||||
@@ -176,6 +176,23 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
|
||||
paymentSlipUrl: t.String()
|
||||
})
|
||||
})
|
||||
.patch("/:id/tracking", async ({ params: { id }, body, set }) => {
|
||||
try {
|
||||
const { trackingNo } = body as any;
|
||||
return await prisma.order.update({
|
||||
where: { id: Number(id) },
|
||||
data: { trackingNo },
|
||||
include: { items: true }
|
||||
});
|
||||
} catch (err) {
|
||||
set.status = 500;
|
||||
return { error: "Failed to update tracking number" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
trackingNo: t.String()
|
||||
})
|
||||
})
|
||||
.get("/customer/:customerId", async ({ params: { customerId } }) => {
|
||||
return await prisma.order.findMany({
|
||||
where: { customerId: Number(customerId) },
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
.get("/", async () => {
|
||||
return await prisma.page.findMany({
|
||||
include: {
|
||||
sections: {
|
||||
orderBy: { order: "asc" }
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.get("/:slug", async ({ params: { slug }, set }) => {
|
||||
const page = await prisma.page.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
sections: {
|
||||
where: { isActive: true },
|
||||
orderBy: { order: "asc" }
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!page) {
|
||||
set.status = 404;
|
||||
return { error: "Page not found" };
|
||||
}
|
||||
|
||||
const populatedSections = await Promise.all(
|
||||
page.sections.map(async (section) => {
|
||||
const payload = section.payload as any;
|
||||
if (!payload) return section;
|
||||
|
||||
const populatedPayload = { ...payload };
|
||||
|
||||
if (section.type === 'PRODUCTS') {
|
||||
let products: any[] = [];
|
||||
const limit = payload.limit || 3;
|
||||
|
||||
const productInclude = {
|
||||
category: true,
|
||||
scent: true,
|
||||
variants: {
|
||||
include: {
|
||||
bomItems: { include: { rawMaterial: true } }
|
||||
},
|
||||
orderBy: { price: "asc" as const }
|
||||
},
|
||||
images: {
|
||||
orderBy: { order: "asc" as const }
|
||||
}
|
||||
};
|
||||
|
||||
if (payload.type === 'featured') {
|
||||
products = await prisma.product.findMany({
|
||||
where: { categoryId: 1 },
|
||||
include: productInclude,
|
||||
take: limit
|
||||
});
|
||||
} else if (payload.type === 'scent' && payload.scentId) {
|
||||
products = await prisma.product.findMany({
|
||||
where: { scentId: Number(payload.scentId) },
|
||||
include: productInclude,
|
||||
take: limit
|
||||
});
|
||||
} else if (payload.type === 'custom' && Array.isArray(payload.productIds)) {
|
||||
const ids = payload.productIds.map(Number);
|
||||
products = await prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: productInclude
|
||||
});
|
||||
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
||||
}
|
||||
|
||||
populatedPayload.products = products;
|
||||
} else if (section.type === 'CATEGORIES') {
|
||||
let categories: any[] = [];
|
||||
if (Array.isArray(payload.categoryIds) && payload.categoryIds.length > 0) {
|
||||
const ids = payload.categoryIds.map(Number);
|
||||
categories = await prisma.category.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: {
|
||||
_count: {
|
||||
select: { products: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
categories.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
||||
} else {
|
||||
categories = await prisma.category.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { products: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
populatedPayload.categories = categories;
|
||||
}
|
||||
|
||||
return {
|
||||
...section,
|
||||
payload: populatedPayload
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...page,
|
||||
sections: populatedSections
|
||||
};
|
||||
})
|
||||
.post("/", async ({ body, set }) => {
|
||||
try {
|
||||
const data = body as any;
|
||||
const { id, slug, title, isPublished } = data;
|
||||
if (id) {
|
||||
return await prisma.page.update({
|
||||
where: { id: Number(id) },
|
||||
data: { slug, title, isPublished: isPublished !== false }
|
||||
});
|
||||
} else {
|
||||
return await prisma.page.create({
|
||||
data: { slug, title, isPublished: isPublished !== false }
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Pages] Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to save page" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.Optional(t.Number()),
|
||||
slug: t.String(),
|
||||
title: t.String(),
|
||||
isPublished: t.Optional(t.Boolean())
|
||||
})
|
||||
})
|
||||
.delete("/:slug", async ({ params: { slug }, set }) => {
|
||||
try {
|
||||
await prisma.page.delete({
|
||||
where: { id: Number(slug) }
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
set.status = 500;
|
||||
return { error: "Failed to delete page" };
|
||||
}
|
||||
})
|
||||
.get("/:slug/sections", async ({ params: { slug }, set }) => {
|
||||
try {
|
||||
return await prisma.pageSection.findMany({
|
||||
where: { pageId: Number(slug) },
|
||||
orderBy: { order: "asc" }
|
||||
});
|
||||
} catch (err) {
|
||||
set.status = 500;
|
||||
return { error: "Failed to fetch page sections" };
|
||||
}
|
||||
})
|
||||
.post("/:slug/sections", async ({ params: { slug }, body, set }) => {
|
||||
try {
|
||||
const pageId = Number(slug);
|
||||
const { type, order, payload, isActive } = body as any;
|
||||
|
||||
const validationError = validateSectionPayload(type, payload);
|
||||
if (validationError) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
}
|
||||
|
||||
return await prisma.pageSection.create({
|
||||
data: {
|
||||
pageId,
|
||||
type: type.toUpperCase(),
|
||||
order,
|
||||
payload,
|
||||
isActive: isActive !== false
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[PageSections] Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to add section" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
type: t.String(),
|
||||
order: t.Number(),
|
||||
payload: t.Any(),
|
||||
isActive: t.Optional(t.Boolean())
|
||||
})
|
||||
})
|
||||
.put("/sections/:sectionId", async ({ params: { sectionId }, body, set }) => {
|
||||
try {
|
||||
const id = Number(sectionId);
|
||||
const { type, order, payload, isActive } = body as any;
|
||||
|
||||
if (payload && type) {
|
||||
const validationError = validateSectionPayload(type, payload);
|
||||
if (validationError) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.pageSection.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type: type ? type.toUpperCase() : undefined,
|
||||
order,
|
||||
payload,
|
||||
isActive
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[PageSections] Update Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to update section" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
type: t.Optional(t.String()),
|
||||
order: t.Optional(t.Number()),
|
||||
payload: t.Optional(t.Any()),
|
||||
isActive: t.Optional(t.Boolean())
|
||||
})
|
||||
})
|
||||
.put("/:slug/sections/reorder", async ({ params: { slug }, body, set }) => {
|
||||
try {
|
||||
const pageId = Number(slug);
|
||||
const { sectionOrders } = body as { sectionOrders: { id: number; order: number }[] };
|
||||
|
||||
await prisma.$transaction(
|
||||
sectionOrders.map((item) =>
|
||||
prisma.pageSection.update({
|
||||
where: { id: item.id, pageId },
|
||||
data: { order: item.order }
|
||||
})
|
||||
)
|
||||
);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error("[PageSections] Reorder Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to reorder sections" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
sectionOrders: t.Array(t.Object({
|
||||
id: t.Number(),
|
||||
order: t.Number()
|
||||
}))
|
||||
})
|
||||
})
|
||||
.post("/newsletter", async ({ body, set }) => {
|
||||
try {
|
||||
const { email } = body as { email: string };
|
||||
if (!email || !email.includes("@")) {
|
||||
set.status = 400;
|
||||
return { error: "Invalid email address" };
|
||||
}
|
||||
|
||||
const sub = await prisma.newsletterSubscription.upsert({
|
||||
where: { email },
|
||||
update: {},
|
||||
create: { email }
|
||||
});
|
||||
return { success: true, data: sub };
|
||||
} catch (err) {
|
||||
console.error("[Newsletter] Error:", err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to subscribe" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.String()
|
||||
})
|
||||
})
|
||||
.delete("/sections/:sectionId", async ({ params: { sectionId }, set }) => {
|
||||
try {
|
||||
await prisma.pageSection.delete({
|
||||
where: { id: Number(sectionId) }
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
set.status = 500;
|
||||
return { error: "Failed to delete section" };
|
||||
}
|
||||
});
|
||||
|
||||
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':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Hero payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Hero payload must contain a string description";
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Hero payload must contain a non-empty string imageUrl";
|
||||
if (typeof payload.tagline !== 'string') return "Hero payload must contain a string tagline";
|
||||
if (typeof payload.buttonText !== 'string') return "Hero payload must contain a string buttonText";
|
||||
if (typeof payload.buttonLink !== 'string') return "Hero payload must contain a string buttonLink";
|
||||
break;
|
||||
case 'CATEGORIES':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Categories payload must contain a non-empty string title";
|
||||
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
|
||||
if (payload.categoryIds !== undefined && !Array.isArray(payload.categoryIds)) {
|
||||
return "categoryIds must be an array of numbers";
|
||||
}
|
||||
break;
|
||||
case 'PRODUCTS':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Products payload must contain a non-empty string title";
|
||||
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
|
||||
if (!['featured', 'scent', 'custom'].includes(payload.type)) {
|
||||
return "Products type must be 'featured', 'scent', or 'custom'";
|
||||
}
|
||||
if (payload.type === 'scent' && typeof payload.scentId !== 'number') {
|
||||
return "Scent products section requires a numeric scentId";
|
||||
}
|
||||
if (payload.type === 'custom' && !Array.isArray(payload.productIds)) {
|
||||
return "Custom products section requires an array of productIds";
|
||||
}
|
||||
if (payload.limit !== undefined && typeof payload.limit !== 'number') {
|
||||
return "Limit must be a number";
|
||||
}
|
||||
if (payload.layout !== undefined && !['grid', 'carousel'].includes(payload.layout)) {
|
||||
return "layout must be 'grid' or 'carousel'";
|
||||
}
|
||||
if (payload.enableQuickShop !== undefined && typeof payload.enableQuickShop !== 'boolean') {
|
||||
return "enableQuickShop must be a boolean";
|
||||
}
|
||||
break;
|
||||
case 'BANNER':
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Banner payload must contain a non-empty string imageUrl";
|
||||
if (payload.linkUrl !== undefined && typeof payload.linkUrl !== 'string') return "linkUrl must be a string";
|
||||
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
|
||||
if (payload.description !== undefined && typeof payload.description !== 'string') return "description must be a string";
|
||||
if (payload.buttonText !== undefined && typeof payload.buttonText !== 'string') return "buttonText must be a string";
|
||||
break;
|
||||
case 'TEXT':
|
||||
if (typeof payload.content !== 'string' || !payload.content.trim()) return "Text payload must contain a non-empty string content";
|
||||
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
|
||||
if (payload.textAlign !== undefined && !['left', 'center', 'right'].includes(payload.textAlign)) {
|
||||
return "textAlign must be 'left', 'center', or 'right'";
|
||||
}
|
||||
break;
|
||||
case 'ANNOUNCEMENT_BAR':
|
||||
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Announcement Bar requires an array of slides";
|
||||
if (payload.slides.some((s: any) => typeof s !== 'string')) return "All slides must be strings";
|
||||
break;
|
||||
case 'NEWSLETTER':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Newsletter payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Newsletter description must be a string";
|
||||
if (typeof payload.buttonText !== 'string' || !payload.buttonText.trim()) return "Newsletter buttonText must be a non-empty string";
|
||||
if (typeof payload.showPrivacyCheckbox !== 'boolean') return "showPrivacyCheckbox must be a boolean";
|
||||
break;
|
||||
case 'EDITORIAL_TILE':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Tile payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Editorial Tile description must be a string";
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Editorial Tile imageUrl must be a non-empty string";
|
||||
if (!['left', 'right'].includes(payload.imagePosition)) return "imagePosition must be 'left' or 'right'";
|
||||
break;
|
||||
case 'EDITORIAL_CAROUSEL':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Carousel payload must contain a non-empty string title";
|
||||
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Editorial Carousel requires an array of slides";
|
||||
for (const [idx, slide] of payload.slides.entries()) {
|
||||
if (typeof slide.title !== 'string' || !slide.title.trim()) return `Slide #${idx+1} must contain a non-empty title`;
|
||||
if (typeof slide.description !== 'string') return `Slide #${idx+1} must contain a description string`;
|
||||
if (typeof slide.imageUrl !== 'string' || !slide.imageUrl.trim()) return `Slide #${idx+1} must contain a non-empty imageUrl`;
|
||||
if (typeof slide.category !== 'string') return `Slide #${idx+1} must contain a category string`;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return `Unknown section type: ${type}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -2,6 +2,11 @@ import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
.get("/scents", async () => {
|
||||
return await prisma.scent.findMany({
|
||||
orderBy: { name: "asc" }
|
||||
});
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
|
||||
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
|
||||
|
||||
Reference in new issue
Block a user