update lang
This commit is contained in:
1 parent
46f00c76a4
commit
f5b60ff63e
30 files changed
+546
-652
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
const prisma = new PrismaClient()
|
||||
async function main() {
|
||||
const cats = await prisma.category.findMany({
|
||||
include: { translations: true }
|
||||
})
|
||||
console.log(JSON.stringify(cats, null, 2))
|
||||
}
|
||||
main()
|
||||
@@ -220,6 +220,8 @@ model StoreSettings {
|
||||
newsletterDescription String?
|
||||
newsletterPlaceholder String?
|
||||
newsletterButtonText String?
|
||||
footerSupportHeading Json?
|
||||
footerCopyrightText Json?
|
||||
translations StoreSettingsTranslation[]
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -98,6 +98,29 @@ async function main() {
|
||||
});
|
||||
process.stdout.write('✅ Seeded Admin User (admin / homaroma123).\n');
|
||||
|
||||
// 0.5 Seed Core Menus
|
||||
const coreMenus = [
|
||||
{ name: "main", items: [{ id: 1, label: "Home", url: "/", order: 1 }, { id: 2, label: "Shop", url: "/shop", order: 2 }, { id: 3, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 4, label: "About Us", url: "/about", order: 4 }] },
|
||||
{ name: "footer-explore", items: [{ id: 5, label: "Home", url: "/", order: 1 }, { id: 6, label: "Shop", url: "/shop", order: 2 }, { id: 7, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 8, label: "About Us", url: "/about", order: 4 }] },
|
||||
{ name: "footer-support", items: [{ id: 9, label: "Track Order", url: "/track-order", order: 1 }, { id: 10, label: "FAQ", url: "/faq", order: 2 }, { id: 11, label: "Contact Us", url: "/contact", order: 3 }, { id: 12, label: "Delivery & Returns", url: "/delivery", order: 4 }] }
|
||||
];
|
||||
|
||||
for (const menu of coreMenus) {
|
||||
const m = await prisma.menu.upsert({
|
||||
where: { name: menu.name },
|
||||
update: {},
|
||||
create: { name: menu.name }
|
||||
});
|
||||
for (const item of menu.items) {
|
||||
await prisma.menuItem.upsert({
|
||||
where: { id: item.id },
|
||||
update: { label: item.label, url: item.url, order: item.order, menuId: m.id },
|
||||
create: { id: item.id, label: item.label, url: item.url, order: item.order, menuId: m.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
process.stdout.write('✅ Seeded Core Menus.\n');
|
||||
|
||||
// 1. Seed Scents & Categories
|
||||
const scents = [];
|
||||
for (const name of scentNames) {
|
||||
@@ -215,6 +238,11 @@ async function main() {
|
||||
process.stdout.write(` - Categories: ${categories.length}\n`);
|
||||
process.stdout.write(` - Products: ${productCount}\n`);
|
||||
process.stdout.write(` - Variants: ${variantCount}\n`);
|
||||
// Fix PostgreSQL sequence desync for tables where we hardcoded IDs
|
||||
process.stdout.write('🔄 Resetting sequences for hardcoded ID tables...\n');
|
||||
await prisma.$executeRawUnsafe(`SELECT setval('"MenuItem_id_seq"', COALESCE((SELECT MAX(id) FROM "MenuItem") + 1, 1), false);`);
|
||||
await prisma.$executeRawUnsafe(`SELECT setval('"ProductImage_id_seq"', COALESCE((SELECT MAX(id) FROM "ProductImage") + 1, 1), false);`);
|
||||
process.stdout.write('✅ Sequences reset successfully.\n');
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
|
||||
async function main() {
|
||||
// 1. Create About Page
|
||||
await prisma.page.upsert({
|
||||
where: { slug: 'about' },
|
||||
update: {},
|
||||
create: {
|
||||
slug: 'about',
|
||||
title: 'About Us',
|
||||
isPublished: true,
|
||||
sections: {
|
||||
create: [
|
||||
{
|
||||
type: 'HERO',
|
||||
order: 1,
|
||||
payload: {
|
||||
tagline: 'Our Story',
|
||||
title: 'Born from a passion for stillness',
|
||||
description: 'HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.',
|
||||
align: 'center',
|
||||
bgImage: '',
|
||||
overlay: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'TEXT',
|
||||
order: 2,
|
||||
payload: {
|
||||
tagline: 'Stats',
|
||||
title: 'Company Stats',
|
||||
content: '45+ Unique Scents\n5 Product Lines\nFounded 2019\n100% Natural Extracts',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'EDITORIAL_TILE',
|
||||
order: 3,
|
||||
payload: {
|
||||
tagline: 'Our Mission',
|
||||
title: 'Transforming spaces into sanctuaries of peace',
|
||||
content: 'We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.\n\nEvery HomAroma product is designed with intention: the bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.',
|
||||
imageSrc: '',
|
||||
imageAlt: 'HomAroma Studio, Bangkok',
|
||||
reverse: false
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'TEXT',
|
||||
order: 4,
|
||||
payload: {
|
||||
tagline: 'What We Stand For',
|
||||
title: 'Three Pillars',
|
||||
content: '1. Pure Origins: Every extract is sourced directly from certified organic farms in Northern Thailand, Vietnam, and Southern France. We work exclusively with small-batch artisan growers.\n\n2. Artisan Craft: Each formula is cold-extracted and hand-blended in our Bangkok studio. No heat processing means every aromatic molecule remains intact and therapeutically potent.\n\n3. Zero Compromise: No synthetic fragrance. No parabens. No animal testing. Our commitment to clean formulation is absolute — what touches your senses should honour your wellbeing.',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'BANNER',
|
||||
order: 5,
|
||||
payload: {
|
||||
title: 'Ready to find your scent?',
|
||||
description: 'Let our Scent Guide help you discover the perfect blend for your moment.',
|
||||
buttonText: 'Take the Scent Guide',
|
||||
buttonLink: '/scent-guide'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Create FAQ Page
|
||||
await prisma.page.upsert({
|
||||
where: { slug: 'faq' },
|
||||
update: {},
|
||||
create: {
|
||||
slug: 'faq',
|
||||
title: 'Frequently Asked Questions',
|
||||
isPublished: true,
|
||||
sections: {
|
||||
create: [
|
||||
{
|
||||
type: 'TEXT',
|
||||
order: 1,
|
||||
payload: {
|
||||
tagline: 'Support',
|
||||
title: 'Frequently Asked Questions',
|
||||
content: 'Everything you need to know about HomAroma products, shipping, and care.',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'ACCORDION',
|
||||
order: 2,
|
||||
payload: {
|
||||
categories: [
|
||||
{
|
||||
name: 'Products',
|
||||
icon: '🌿',
|
||||
items: [
|
||||
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
||||
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
||||
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' },
|
||||
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation, ideal for large rooms and commercial spaces.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Usage',
|
||||
icon: '💡',
|
||||
items: [
|
||||
{ q: 'How do I use a Reed Diffuser?', a: 'Remove the stopper, place the reeds into the bottle, and let them absorb the oil for 1 hour before flipping. For optimal fragrance, flip the reeds every 2–3 days. Keep away from direct sunlight and air conditioning vents for longest lasting performance.' },
|
||||
{ q: 'Can I mix different scents?', a: 'Yes! Blending scents is an art form we encourage. A general rule: combine no more than 3 scents at a time, using one dominant (60%), one supporting (30%), and one accent (10%). Our Scent Guide can help you find complementary combinations.' },
|
||||
{ q: 'How long should I run my aroma diffuser?', a: 'For best results and to avoid olfactory fatigue, we recommend 30–60 minute sessions with 30-minute breaks in between. This also extends product life and prevents overwhelming fragrance build-up in small spaces.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Shipping & Orders',
|
||||
icon: '📦',
|
||||
items: [
|
||||
{ q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' },
|
||||
{ q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' },
|
||||
{ q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@homaroma.shop with your order number.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Scent Guide',
|
||||
icon: '🧪',
|
||||
items: [
|
||||
{ q: 'How do I choose the right scent for my space?', a: 'Consider the function of the space first. Bedrooms benefit from calming scents like lavender and chamomile. Living rooms work well with warm, welcoming notes like sandalwood and vanilla. Home offices pair best with focus-enhancing eucalyptus and citrus. Use our interactive Scent Guide for personalized recommendations.' },
|
||||
{ q: 'Can I request custom scent blending?', a: 'Yes! Our bespoke blending service is available for orders of 6 units or more. Contact us at least 10 business days before your desired delivery date. Custom blending fees start at ฿500 per unique formula.' },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'BANNER',
|
||||
order: 3,
|
||||
payload: {
|
||||
title: 'Still have questions?',
|
||||
description: 'Our team is here to help — usually responds within 2 hours.',
|
||||
buttonText: 'Contact Us',
|
||||
buttonLink: '/contact'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Create Contact Page
|
||||
await prisma.page.upsert({
|
||||
where: { slug: 'contact' },
|
||||
update: {},
|
||||
create: {
|
||||
slug: 'contact',
|
||||
title: 'Contact Us',
|
||||
isPublished: true,
|
||||
sections: {
|
||||
create: [
|
||||
{
|
||||
type: 'TEXT',
|
||||
order: 1,
|
||||
payload: {
|
||||
tagline: 'Get in Touch',
|
||||
title: 'Contact Us',
|
||||
content: 'Have a question, custom request, or partnership idea? We\'d love to hear from you.',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'CONTACT_INFO',
|
||||
order: 2,
|
||||
payload: {
|
||||
title: 'Reach Us Directly',
|
||||
items: [
|
||||
{ icon: '📧', label: 'Email', value: 'hello@homaroma.shop', href: 'mailto:hello@homaroma.shop' },
|
||||
{ icon: '📱', label: 'LINE Official', value: '@homaroma', href: '#' },
|
||||
{ icon: '📸', label: 'Instagram', value: '@homaroma', href: '#' },
|
||||
{ icon: '📍', label: 'Studio', value: 'Bangkok, Thailand', href: '#' },
|
||||
],
|
||||
responseTime: {
|
||||
title: 'Response Time',
|
||||
email: '~2 hours',
|
||||
line: '~30 minutes',
|
||||
instagram: '~4 hours',
|
||||
hours: 'Mon–Sat, 9:00–18:00 (ICT)'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'CONTACT_FORM',
|
||||
order: 3,
|
||||
payload: {
|
||||
subjects: ['Product Inquiry', 'Order Support', 'Custom Blending', 'Wholesale', 'Partnership', 'Other'],
|
||||
buttonText: 'Send Message'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Pages created successfully.');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -7,7 +7,8 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
return await prisma.menu.findMany({
|
||||
include: {
|
||||
items: {
|
||||
orderBy: { order: "asc" }
|
||||
orderBy: { order: "asc" },
|
||||
include: { translations: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -17,19 +18,12 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
where: { name },
|
||||
include: {
|
||||
items: {
|
||||
orderBy: { order: "asc" }
|
||||
orderBy: { order: "asc" },
|
||||
include: { translations: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!menu) {
|
||||
// Return fallback defaults instead of 404 to prevent frontend breakage
|
||||
if (name === "main") {
|
||||
return { name, items: [{ id: 1, label: "Home", url: "/", order: 1 }, { id: 2, label: "Shop", url: "/shop", order: 2 }, { id: 3, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 4, label: "About Us", url: "/about", order: 4 }] };
|
||||
} else if (name === "footer-explore") {
|
||||
return { name, items: [{ id: 5, label: "Home", url: "/", order: 1 }, { id: 6, label: "Shop", url: "/shop", order: 2 }, { id: 7, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 8, label: "About Us", url: "/about", order: 4 }] };
|
||||
} else if (name === "footer-support") {
|
||||
return { name, items: [{ id: 9, label: "Track Order", url: "/track-order", order: 1 }, { id: 10, label: "FAQ", url: "/faq", order: 2 }, { id: 11, label: "Contact Us", url: "/contact", order: 3 }, { id: 12, label: "Delivery & Returns", url: "/delivery", order: 4 }] };
|
||||
}
|
||||
return { name, items: [] };
|
||||
}
|
||||
return menu;
|
||||
@@ -65,28 +59,60 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
})
|
||||
.put("/:name", async ({ params: { name }, body, set }) => {
|
||||
try {
|
||||
const { items } = body as { items: { id?: number; label: string; url: string; order: number }[] };
|
||||
const { items } = body as { items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[] };
|
||||
let menu = await prisma.menu.findUnique({ where: { name } });
|
||||
|
||||
if (!menu) {
|
||||
menu = await prisma.menu.create({ data: { name } });
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.menuItem.deleteMany({ where: { menuId: menu.id } }),
|
||||
prisma.menuItem.createMany({
|
||||
data: items.map(item => ({
|
||||
menuId: menu.id,
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order
|
||||
}))
|
||||
})
|
||||
]);
|
||||
await prisma.$transaction(async (tx: any) => {
|
||||
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu!.id } });
|
||||
const existingItemIds = existingItems.map((i: any) => i.id);
|
||||
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
|
||||
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
|
||||
|
||||
if (itemsToDelete.length > 0) {
|
||||
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id && existingItemIds.includes(item.id)) {
|
||||
await tx.menuItem.update({
|
||||
where: { id: item.id },
|
||||
data: { label: item.label, url: item.url, order: item.order }
|
||||
});
|
||||
if (item.translations?.length) {
|
||||
for (const t of item.translations) {
|
||||
await tx.menuItemTranslation.upsert({
|
||||
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
|
||||
update: { label: t.label },
|
||||
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tx.menuItem.create({
|
||||
data: {
|
||||
menuId: menu!.id,
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
translations: item.translations?.length ? {
|
||||
create: item.translations.map(t => ({
|
||||
langCode: t.langCode,
|
||||
label: t.label
|
||||
}))
|
||||
} : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return await prisma.menu.findUnique({
|
||||
where: { name },
|
||||
include: { items: { orderBy: { order: "asc" } } }
|
||||
include: { items: { orderBy: { order: "asc" }, include: { translations: true } } }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -99,7 +125,11 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
id: t.Optional(t.Number()),
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number()
|
||||
order: t.Number(),
|
||||
translations: t.Optional(t.Array(t.Object({
|
||||
langCode: t.String(),
|
||||
label: t.String()
|
||||
})))
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -110,6 +140,12 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
return { error: "Invalid menu item ID" };
|
||||
}
|
||||
|
||||
const menuItem = await prisma.menuItem.findUnique({ where: { id } });
|
||||
if (!menuItem) {
|
||||
set.status = 404;
|
||||
return { error: "Menu item not found. Please ensure database is seeded or the item exists." };
|
||||
}
|
||||
|
||||
const payload = body as any;
|
||||
const results: any[] = [];
|
||||
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
||||
|
||||
@@ -51,7 +51,9 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
newsletterTitle: t.Optional(t.String()),
|
||||
newsletterDescription: t.Optional(t.String()),
|
||||
newsletterPlaceholder: t.Optional(t.String()),
|
||||
newsletterButtonText: t.Optional(t.String())
|
||||
newsletterButtonText: t.Optional(t.String()),
|
||||
footerSupportHeading: t.Optional(t.Any()),
|
||||
footerCopyrightText: t.Optional(t.Any())
|
||||
})
|
||||
})
|
||||
.patch("/translations", async ({ body, set }) => {
|
||||
|
||||
Reference in new issue
Block a user