chore: clean up obsolete scripts and temp files
This commit is contained in:
1 parent
e1d793ce37
commit
e405af357c
41 files changed
-1905
No files matched your search
@@ -1,89 +0,0 @@
|
||||
ตะไคร้,120mlSeller SKU: 120-ตะไคร้-120ml
|
||||
ลาเวนเดอร์,120mlSeller SKU: 120-ลาเวนเดอร์-120ml
|
||||
ลาเวนเดอร์,30mlSeller SKU: 30-ลาเวนเดอร์-30ml
|
||||
วานิลลา,120mlSeller SKU: 120-วานิลลา-120ml
|
||||
วานิลลา,30mlSeller SKU: 30-วานิลลา-30ml
|
||||
ซากุระ,120mlSeller SKU: 120-ซากุระ-120ml
|
||||
ซากุระ,30mlSeller SKU: 30-ซากุระ-30ml
|
||||
มะลิ,120mlSeller SKU: 120-มะลิ-120ml
|
||||
มะลิ,30mlSeller SKU: 30-มะลิ-30ml
|
||||
ยูคาลิปตัส,120mlSeller SKU: 120-ยูคาลิปตัส-120ml
|
||||
ยูคาลิปตัส,30mlSeller SKU: 30-ยูคาลิปตัส-30ml
|
||||
เฟรชลาเวนเดอร์,120mlSeller SKU: 120-เฟรชลาเวนเดอร์-120ml
|
||||
เฟรชลาเวนเดอร์,30mlSeller SKU: 30-เฟรชลาเวนเดอร์-30ml
|
||||
เปเปอร์มิ้น,120mlSeller SKU: 120-เปเปอร์มิ้น-120ml
|
||||
เปเปอร์มิ้น,30mlSeller SKU: 30-เปเปอร์มิ้น-30ml
|
||||
โอเชี่ยน,120mlSeller SKU: 120-โอเชี่ยน-120ml
|
||||
โอเชี่ยน,30mlSeller SKU: 30-โอเชี่ยน-30ml
|
||||
เซเว่นซี,120mlSeller SKU: 120-เซเว่นซี-120ml
|
||||
เซเว่นซี,30mlSeller SKU: 30-เซเว่นซี-30ml
|
||||
ดอกแก้ว,120mlSeller SKU: 120-ดอกแก้ว-120ml
|
||||
ดอกแก้ว,30mlSeller SKU: 30-ดอกแก้ว-30ml
|
||||
ดอกโมก,120mlSeller SKU: 120-ดอกโมก-120ml
|
||||
ดอกโมก,30mlSeller SKU: 30-ดอกโมก-30ml
|
||||
ลีลาวดี,120mlSeller SKU: 120-ลีลาวดี-120ml
|
||||
ลีลาวดี,30mlSeller SKU: 30-ลีลาวดี-30ml
|
||||
ส้ม,120mlSeller SKU: 120-ส้ม-120ml
|
||||
ส้ม,30mlSeller SKU: 30-ส้ม-30ml
|
||||
มะนาว,120mlSeller SKU: 120-มะนาว-120ml
|
||||
มะนาว,30mlSeller SKU: 30-มะนาว-30ml
|
||||
แคนตาลูป,120mlSeller SKU: 120-แคนตาลูป-120ml
|
||||
แคนตาลูป,30mlSeller SKU: 30-แคนตาลูป-30ml
|
||||
แอปเปิ้ลแดง,120mlSeller SKU: 120-แอปเปิ้ลแดง-120ml
|
||||
แอปเปิ้ลแดง,30mlSeller SKU: 30-แอปเปิ้ลแดง-30ml
|
||||
แอปเปิ้ลเขียว,120mlSeller SKU: 120-แอปเปิ้ลเขียว-120ml
|
||||
แอปเปิ้ลเขียว,30mlSeller SKU: 30-แอปเปิ้ลเขียว-30ml
|
||||
ดอกลิลลี่,120mlSeller SKU: 120-ดอกลิลลี่-120ml
|
||||
ดอกลิลลี่,30mlSeller SKU: 30-ดอกลิลลี่-30ml
|
||||
โรสเมรี่,120mlSeller SKU: 120-โรสเมรี่-120ml
|
||||
โรสเมรี่,30mlSeller SKU: 30-โรสเมรี่-30ml
|
||||
อัญชัน,120mlSeller SKU: 120-อัญชัน-120ml
|
||||
อัญชัน,30mlSeller SKU: 30-อัญชัน-30ml
|
||||
กุหลาบแดง,120mlSeller SKU: 120-กุหลาบแดง-120ml
|
||||
กุหลาบแดง,30mlSeller SKU: 30-กุหลาบแดง-30ml
|
||||
หมากฝรั่ง,120mlSeller SKU: 120-หมากฝรั่ง-120ml
|
||||
หมากฝรั่ง,30mlSeller SKU: 30-หมากฝรั่ง-30ml
|
||||
ผลไม้รวม,120mlSeller SKU: 120-ผลไม้รวม-120ml
|
||||
ผลไม้รวม,30mlSeller SKU: 30-ผลไม้รวม-30ml
|
||||
มะกรูด,120mlSeller SKU: 120-มะกรูด-120ml
|
||||
มะกรูด,30mlSeller SKU: 30-มะกรูด-30ml
|
||||
แซนด้าวูส,120mlSeller SKU: 120-แซนด้าวูส-120ml
|
||||
แซนด้าวูส,30mlSeller SKU: 30-แซนด้าวูส-30ml
|
||||
ดอกปีบ,120mlSeller SKU: 120-ดอกปีบ-120ml
|
||||
ดอกปีบ,30mlSeller SKU: 30-ดอกปีบ-30ml
|
||||
ดอกบัว,120mlSeller SKU: 120-ดอกบัว-120ml
|
||||
ดอกบัว,30mlSeller SKU: 30-ดอกบัว-30ml
|
||||
โอโซน,120mlSeller SKU: 120-โอโซน-120ml
|
||||
โอโซน,30mlSeller SKU: 30-โอโซน-30ml
|
||||
ไทยสปา,120mlSeller SKU: 120-ไทยสปา-120ml
|
||||
ไทยสปา,30mlSeller SKU: 30-ไทยสปา-30ml
|
||||
กล้วย,120mlSeller SKU: 120-กล้วย-120ml
|
||||
กล้วย,30mlSeller SKU: 30-กล้วย-30ml
|
||||
กาแฟ,120mlSeller SKU: 120-กาแฟ-120ml
|
||||
กาแฟ,30mlSeller SKU: 30-กาแฟ-30ml
|
||||
ดอกชมนาด,120mlSeller SKU: 120-ดอกชมนาด-120ml
|
||||
ดอกชมนาด,30mlSeller SKU: 30-ดอกชมนาด-30ml
|
||||
กลิ่นน้ำนม,120mlSeller SKU: 120-กลิ่นน้ำนม-120ml
|
||||
กลิ่นน้ำนม,30mlSeller SKU: 30-กลิ่นน้ำนม-30ml
|
||||
ดอกการะเวก,120mlSeller SKU: 120-ดอกการะเวก-120ml
|
||||
ดอกการะเวก,30mlSeller SKU: 30-ดอกการะเวก-30ml
|
||||
ดอกจำปา,120mlSeller SKU: 120-ดอกจำปา-120ml
|
||||
ดอกจำปา,30mlSeller SKU: 30-ดอกจำปา-30ml
|
||||
ดอกพิกุล,120mlSeller SKU: 120-ดอกพิกุล-120ml
|
||||
ดอกพิกุล,30mlSeller SKU: 30-ดอกพิกุล-30ml
|
||||
ดอกพุด,120mlSeller SKU: 120-ดอกพุด-120ml
|
||||
ดอกพุด,30mlSeller SKU: 30-ดอกพุด-30ml
|
||||
ดอกราตรี,120mlSeller SKU: 120-ดอกราตรี-120ml
|
||||
ดอกราตรี,30mlSeller SKU: 30-ดอกราตรี-30ml
|
||||
ดอกลำดวน,120mlSeller SKU: 120-ดอกลำดวน-120ml
|
||||
ดอกลำดวน,30mlSeller SKU: 30-ดอกลำดวน-30ml
|
||||
ทับทิม,120mlSeller SKU: 120-ทับทิม-120ml
|
||||
ทับทิม,30mlSeller SKU: 30-ทับทิม-30ml
|
||||
มะละกอสุก,120mlSeller SKU: 120-มะละกอสุก-120ml
|
||||
มะละกอสุก,30mlSeller SKU: 30-มะละกอสุก-30ml
|
||||
ดอกราชาวดี,120mlSeller SKU: 120-ดอกราชาวดี-120ml
|
||||
ดอกราชาวดี,30mlSeller SKU: 30-ดอกราชาวดี-30ml
|
||||
กลิ่นลุกพีช,120mlSeller SKU: 120-กลิ่นลุกพีช-120ml
|
||||
กลิ่นลุกพีช,30mlSeller SKU: 30-กลิ่นลุกพีช-30ml
|
||||
กลิ่นองุ่น,120mlSeller SKU: 120-กลิ่นองุ่น-120ml
|
||||
กลิ่นองุ่น,30ml
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=fix-menus.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"fix-menus.d.ts","sourceRoot":"","sources":["fix-menus.ts"],"names":[],"mappings":""}
|
||||
@@ -1,46 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
function fixNode(node) {
|
||||
if (node.translations) {
|
||||
const labelKey = node.title !== undefined ? 'title' : 'label';
|
||||
if (typeof node[labelKey] === 'string') {
|
||||
node[labelKey] = { en: node[labelKey] };
|
||||
}
|
||||
node[labelKey] = { ...(node[labelKey] || {}), ...node.translations };
|
||||
delete node.translations;
|
||||
}
|
||||
if (node.columns && Array.isArray(node.columns)) {
|
||||
node.columns.forEach((col) => fixNode(col));
|
||||
}
|
||||
if (node.links && Array.isArray(node.links)) {
|
||||
node.links.forEach((link) => fixNode(link));
|
||||
}
|
||||
if (node.items && Array.isArray(node.items)) {
|
||||
node.items.forEach((item) => fixNode(item));
|
||||
}
|
||||
}
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
if (!menu || !menu.payload) {
|
||||
console.log('No main menu payload found');
|
||||
return;
|
||||
}
|
||||
const payload = menu.payload;
|
||||
console.log('Original payload length:', payload.length);
|
||||
payload.forEach(fixNode);
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
console.log('Menu fixed successfully');
|
||||
}
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
//# sourceMappingURL=fix-menus.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"fix-menus.js","sourceRoot":"","sources":["fix-menus.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC,SAAS,OAAO,CAAC,IAAS;IACxB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAC9D,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ,EAAE,CAAC;YACvC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACrE,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;QACxC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;KACxB,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAgB,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAExD,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEzB,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACvB,IAAI,EAAE,EAAE,OAAO,EAAE;KAClB,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AACzC,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE;IACT,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}
|
||||
@@ -1,54 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
|
||||
function fixNode(node: any) {
|
||||
if (node.translations) {
|
||||
const labelKey = node.title !== undefined ? 'title' : 'label';
|
||||
if (typeof node[labelKey] === 'string') {
|
||||
node[labelKey] = { en: node[labelKey] };
|
||||
}
|
||||
node[labelKey] = { ...(node[labelKey] || {}), ...node.translations };
|
||||
delete node.translations;
|
||||
}
|
||||
|
||||
if (node.columns && Array.isArray(node.columns)) {
|
||||
node.columns.forEach((col: any) => fixNode(col));
|
||||
}
|
||||
if (node.links && Array.isArray(node.links)) {
|
||||
node.links.forEach((link: any) => fixNode(link));
|
||||
}
|
||||
if (node.items && Array.isArray(node.items)) {
|
||||
node.items.forEach((item: any) => fixNode(item));
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
|
||||
if (!menu || !menu.payload) {
|
||||
console.log('No main menu payload found');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = menu.payload as any[];
|
||||
console.log('Original payload length:', payload.length);
|
||||
|
||||
payload.forEach(fixNode);
|
||||
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
|
||||
console.log('Menu fixed successfully');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=fix-scent-guide.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"fix-scent-guide.d.ts","sourceRoot":"","sources":["fix-scent-guide.ts"],"names":[],"mappings":""}
|
||||
@@ -1,53 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
import crypto from 'crypto';
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
if (!menu || !menu.payload) {
|
||||
console.error('Menu main not found or payload is empty');
|
||||
return;
|
||||
}
|
||||
const payload = menu.payload;
|
||||
let updated = false;
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const node = payload[i];
|
||||
if (node.nodeId === 'b4b2faf8-8881-4ca9-a8d5-a88054a37385') {
|
||||
if (node.links && !node.columns) {
|
||||
console.log('Fixing Scent Guide node...');
|
||||
const newColumn = {
|
||||
nodeId: crypto.randomUUID(),
|
||||
title: {
|
||||
en: 'Essential Oils',
|
||||
th: 'น้ำมันหอมระเหย',
|
||||
ja: 'エッセンシャルオイル',
|
||||
ko: '에센셜 오일',
|
||||
vi: 'Tinh dầu',
|
||||
zh: '精油'
|
||||
},
|
||||
links: node.links
|
||||
};
|
||||
node.columns = [newColumn];
|
||||
delete node.links;
|
||||
updated = true;
|
||||
}
|
||||
else {
|
||||
console.log('Scent Guide node already has columns or no links to fix.');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
console.log('Successfully updated main menu payload.');
|
||||
}
|
||||
else {
|
||||
console.log('No updates were necessary.');
|
||||
}
|
||||
}
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
//# sourceMappingURL=fix-scent-guide.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"fix-scent-guide.js","sourceRoot":"","sources":["fix-scent-guide.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AACvC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;QACxC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;KACxB,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACzD,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAgB,CAAC;IACtC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,KAAK,sCAAsC,EAAE,CAAC;YAC3D,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAChC,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;gBAE1C,MAAM,SAAS,GAAG;oBAChB,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;oBAC3B,KAAK,EAAE;wBACL,EAAE,EAAE,gBAAgB;wBACpB,EAAE,EAAE,gBAAgB;wBACpB,EAAE,EAAE,YAAY;wBAChB,EAAE,EAAE,QAAQ;wBACZ,EAAE,EAAE,UAAU;wBACd,EAAE,EAAE,IAAI;qBACT;oBACD,KAAK,EAAE,IAAI,CAAC,KAAK;iBAClB,CAAC;gBAEF,IAAI,CAAC,OAAO,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC3B,OAAO,IAAI,CAAC,KAAK,CAAC;gBAClB,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;YACvB,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;YACvB,IAAI,EAAE,EAAE,OAAO,EAAE;SAClB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;KACpB,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC"}
|
||||
@@ -1,58 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
import crypto from 'crypto';
|
||||
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
|
||||
if (!menu || !menu.payload) {
|
||||
console.error('Menu main not found or payload is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = menu.payload as any[];
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const node = payload[i];
|
||||
if (node.nodeId === 'b4b2faf8-8881-4ca9-a8d5-a88054a37385') {
|
||||
if (node.links && !node.columns) {
|
||||
console.log('Fixing Scent Guide node...');
|
||||
|
||||
const newColumn = {
|
||||
nodeId: crypto.randomUUID(),
|
||||
title: {
|
||||
en: 'Essential Oils',
|
||||
th: 'น้ำมันหอมระเหย',
|
||||
ja: 'エッセンシャルオイル',
|
||||
ko: '에센셜 오일',
|
||||
vi: 'Tinh dầu',
|
||||
zh: '精油'
|
||||
},
|
||||
links: node.links
|
||||
};
|
||||
|
||||
node.columns = [newColumn];
|
||||
delete node.links;
|
||||
updated = true;
|
||||
} else {
|
||||
console.log('Scent Guide node already has columns or no links to fix.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
console.log('Successfully updated main menu payload.');
|
||||
} else {
|
||||
console.log('No updates were necessary.');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=migrate-i18n.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-i18n.d.ts","sourceRoot":"","sources":["migrate-i18n.ts"],"names":[],"mappings":""}
|
||||
@@ -1,41 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
async function main() {
|
||||
console.log('Starting i18n migration...');
|
||||
const products = await prisma.product.findMany();
|
||||
console.log(`Found ${products.length} products to migrate.`);
|
||||
for (const product of products) {
|
||||
const existingTranslation = await prisma.productTranslation.findUnique({
|
||||
where: {
|
||||
productId_langCode: {
|
||||
productId: product.id,
|
||||
langCode: 'th',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existingTranslation) {
|
||||
await prisma.productTranslation.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
langCode: 'th',
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
details: product.details,
|
||||
},
|
||||
});
|
||||
console.log(`Created Thai translation for product ID: ${product.id}`);
|
||||
}
|
||||
else {
|
||||
console.log(`Thai translation already exists for product ID: ${product.id}`);
|
||||
}
|
||||
}
|
||||
console.log('Migration completed successfully.');
|
||||
}
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
//# sourceMappingURL=migrate-i18n.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-i18n.js","sourceRoot":"","sources":["migrate-i18n.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAE1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,SAAS,QAAQ,CAAC,MAAM,uBAAuB,CAAC,CAAC;IAE7D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,mBAAmB,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC;YACrE,KAAK,EAAE;gBACL,kBAAkB,EAAE;oBAClB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACzB,MAAM,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC;gBACrC,IAAI,EAAE;oBACJ,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,QAAQ,EAAE,IAAI;oBACd,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,OAAO,EAAE,OAAO,CAAC,OAAO;iBACzB;aACF,CAAC,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,4CAA4C,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,mDAAmD,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}
|
||||
@@ -1,45 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
|
||||
async function main() {
|
||||
console.log('Starting i18n migration...');
|
||||
|
||||
const products = await prisma.product.findMany();
|
||||
console.log(`Found ${products.length} products to migrate.`);
|
||||
|
||||
for (const product of products) {
|
||||
const existingTranslation = await prisma.productTranslation.findUnique({
|
||||
where: {
|
||||
productId_langCode: {
|
||||
productId: product.id,
|
||||
langCode: 'th',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingTranslation) {
|
||||
await prisma.productTranslation.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
langCode: 'th',
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
details: product.details,
|
||||
},
|
||||
});
|
||||
console.log(`Created Thai translation for product ID: ${product.id}`);
|
||||
} else {
|
||||
console.log(`Thai translation already exists for product ID: ${product.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Migration completed successfully.');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=migrate-pages.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-pages.d.ts","sourceRoot":"","sources":["migrate-pages.ts"],"names":[],"mappings":""}
|
||||
@@ -1,210 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
async function main() {
|
||||
console.log('Migrating pages to CMS...');
|
||||
// Delete existing to force fresh payload insert
|
||||
await prisma.page.deleteMany({
|
||||
where: { slug: { in: ['about', 'faq', 'contact'] } }
|
||||
});
|
||||
// 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',
|
||||
imageUrl: '/images/hero.png',
|
||||
overlay: true,
|
||||
buttonText: 'Discover More',
|
||||
buttonLink: '/shop'
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
description: '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.',
|
||||
imageUrl: '/images/hero.png',
|
||||
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',
|
||||
imageUrl: '/images/hero.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
// 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: '🌿',
|
||||
questions: [
|
||||
{ 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: '💡',
|
||||
questions: [
|
||||
{ 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: '📦',
|
||||
questions: [
|
||||
{ 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: '🧪',
|
||||
questions: [
|
||||
{ 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',
|
||||
imageUrl: '/images/hero.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
// 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: {
|
||||
address: 'Bangkok, Thailand',
|
||||
email: 'hello@homaroma.shop',
|
||||
line: '@homaroma',
|
||||
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();
|
||||
});
|
||||
//# sourceMappingURL=migrate-pages.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-pages.js","sourceRoot":"","sources":["migrate-pages.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,216 +0,0 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
|
||||
async function main() {
|
||||
console.log('Migrating pages to CMS...');
|
||||
|
||||
// Delete existing to force fresh payload insert
|
||||
await prisma.page.deleteMany({
|
||||
where: { slug: { in: ['about', 'faq', 'contact'] } }
|
||||
});
|
||||
|
||||
// 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',
|
||||
imageUrl: '/images/hero.png',
|
||||
overlay: true,
|
||||
buttonText: 'Discover More',
|
||||
buttonLink: '/shop'
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
description: '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.',
|
||||
imageUrl: '/images/hero.png',
|
||||
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',
|
||||
imageUrl: '/images/hero.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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: '🌿',
|
||||
questions: [
|
||||
{ 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: '💡',
|
||||
questions: [
|
||||
{ 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: '📦',
|
||||
questions: [
|
||||
{ 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: '🧪',
|
||||
questions: [
|
||||
{ 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',
|
||||
imageUrl: '/images/hero.png'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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: {
|
||||
address: 'Bangkok, Thailand',
|
||||
email: 'hello@homaroma.shop',
|
||||
line: '@homaroma',
|
||||
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();
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=migrate-production.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-production.d.ts","sourceRoot":"","sources":["migrate-production.ts"],"names":[],"mappings":""}
|
||||
@@ -1,91 +0,0 @@
|
||||
import axios from 'axios';
|
||||
const BACKEND_URL = "https://api.homaroma.shop/api";
|
||||
const ADMIN_USERNAME = "admin";
|
||||
const ADMIN_PASSWORD = "homaroma123";
|
||||
async function main() {
|
||||
console.log("Authenticating...");
|
||||
const authRes = await axios.post(`${BACKEND_URL}/auth/login`, {
|
||||
username: ADMIN_USERNAME,
|
||||
password: ADMIN_PASSWORD,
|
||||
});
|
||||
const token = authRes.data.token;
|
||||
const api = axios.create({
|
||||
baseURL: BACKEND_URL,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const pages = [
|
||||
{
|
||||
slug: 'about', title: 'About Us', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'faq', title: 'Frequently Asked Questions', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
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: '🌿', questions: [{ q: 'Are your products 100% natural?', a: 'Yes. All our products are formulated using 100% natural botanical extracts, pure essential oils, and plant-derived carriers. We never use synthetic fragrances or phthalates.' }, { q: 'Are your products pet-friendly?', a: 'While natural, some essential oils can be sensitive to certain pets (like cats and eucalyptus). Please consult your vet, and always ensure your pet can leave the room when a diffuser is running.' }] }, { name: 'Shipping', icon: '📦', questions: [{ q: 'Do you ship internationally?', a: 'Currently, we ship within Thailand and selected countries in Southeast Asia. International shipping expansion is planned for late 2026.' }, { q: 'How long does shipping take?', a: 'Standard domestic shipping within Thailand takes 2-3 business days. Express shipping takes 1-2 business days.' }] }] }
|
||||
},
|
||||
{
|
||||
type: 'BANNER', order: 3,
|
||||
payload: { title: 'Still have questions?', description: 'Our support team is available Mon-Sat, 9am - 6pm.', buttonText: 'Contact Us', buttonLink: '/contact' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'contact', title: 'Contact Us', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
type: 'TEXT', order: 1,
|
||||
payload: { tagline: 'Get in Touch', title: 'We are here to help', content: 'Whether you need help selecting a scent or have questions about an order, our team is ready to assist you.', align: 'center' }
|
||||
},
|
||||
{
|
||||
type: 'CONTACT_INFO', order: 2,
|
||||
payload: { address: 'HomAroma Studio\n123 Sukhumvit Road\nBangkok 10110, Thailand', email: 'hello@homaroma.shop', line: '@homaroma', hours: 'Mon-Sat: 9:00 AM - 6:00 PM' }
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
for (const page of pages) {
|
||||
console.log(`Creating page: ${page.slug}`);
|
||||
try {
|
||||
const pageRes = await api.post('/pages', { slug: page.slug, title: page.title, isPublished: page.isPublished });
|
||||
const pageId = pageRes.data.id;
|
||||
console.log(`Created page ${page.slug} with ID ${pageId}`);
|
||||
for (const section of page.sections) {
|
||||
console.log(` Adding section ${section.type}`);
|
||||
await api.post(`/pages/${pageId}/sections`, { type: section.type, order: section.order, payload: section.payload, isActive: true });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`Error on ${page.slug}:`, err.response?.data || err.message);
|
||||
}
|
||||
}
|
||||
console.log("Migration to Production complete!");
|
||||
}
|
||||
main();
|
||||
//# sourceMappingURL=migrate-production.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"migrate-production.js","sourceRoot":"","sources":["migrate-production.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,94 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const BACKEND_URL = "https://api.homaroma.shop/api";
|
||||
const ADMIN_USERNAME = "admin";
|
||||
const ADMIN_PASSWORD = "homaroma123";
|
||||
|
||||
async function main() {
|
||||
console.log("Authenticating...");
|
||||
const authRes = await axios.post(`${BACKEND_URL}/auth/login`, {
|
||||
username: ADMIN_USERNAME,
|
||||
password: ADMIN_PASSWORD,
|
||||
});
|
||||
const token = authRes.data.token;
|
||||
const api = axios.create({
|
||||
baseURL: BACKEND_URL,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const pages = [
|
||||
{
|
||||
slug: 'about', title: 'About Us', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'faq', title: 'Frequently Asked Questions', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
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: '🌿', questions: [{ q: 'Are your products 100% natural?', a: 'Yes. All our products are formulated using 100% natural botanical extracts, pure essential oils, and plant-derived carriers. We never use synthetic fragrances or phthalates.' }, { q: 'Are your products pet-friendly?', a: 'While natural, some essential oils can be sensitive to certain pets (like cats and eucalyptus). Please consult your vet, and always ensure your pet can leave the room when a diffuser is running.' }] }, { name: 'Shipping', icon: '📦', questions: [{ q: 'Do you ship internationally?', a: 'Currently, we ship within Thailand and selected countries in Southeast Asia. International shipping expansion is planned for late 2026.' }, { q: 'How long does shipping take?', a: 'Standard domestic shipping within Thailand takes 2-3 business days. Express shipping takes 1-2 business days.' }] }] }
|
||||
},
|
||||
{
|
||||
type: 'BANNER', order: 3,
|
||||
payload: { title: 'Still have questions?', description: 'Our support team is available Mon-Sat, 9am - 6pm.', buttonText: 'Contact Us', buttonLink: '/contact' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: 'contact', title: 'Contact Us', isPublished: true,
|
||||
sections: [
|
||||
{
|
||||
type: 'TEXT', order: 1,
|
||||
payload: { tagline: 'Get in Touch', title: 'We are here to help', content: 'Whether you need help selecting a scent or have questions about an order, our team is ready to assist you.', align: 'center' }
|
||||
},
|
||||
{
|
||||
type: 'CONTACT_INFO', order: 2,
|
||||
payload: { address: 'HomAroma Studio\n123 Sukhumvit Road\nBangkok 10110, Thailand', email: 'hello@homaroma.shop', line: '@homaroma', hours: 'Mon-Sat: 9:00 AM - 6:00 PM' }
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
for (const page of pages) {
|
||||
console.log(`Creating page: ${page.slug}`);
|
||||
try {
|
||||
const pageRes = await api.post('/pages', { slug: page.slug, title: page.title, isPublished: page.isPublished });
|
||||
const pageId = pageRes.data.id;
|
||||
console.log(`Created page ${page.slug} with ID ${pageId}`);
|
||||
for (const section of page.sections) {
|
||||
console.log(` Adding section ${section.type}`);
|
||||
await api.post(`/pages/${pageId}/sections`, { type: section.type, order: section.order, payload: section.payload, isActive: true });
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`Error on ${page.slug}:`, err.response?.data || err.message);
|
||||
}
|
||||
}
|
||||
console.log("Migration to Production complete!");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,58 +0,0 @@
|
||||
const CONFIG = {
|
||||
email: 'ti.ddc.2026@gmail.com', // อีเมลที่ใช้สมัคร Cloudflare
|
||||
apiKey: 'cfut_fZ4ct5szqJbJxpdEfPT0Q5790SqjKZuU4DlbTukn769a34d8',
|
||||
zoneId: '82a3c279a9f53ab8a5531848031c7629',
|
||||
recordName: 'homaroma.shop',
|
||||
proxied: false
|
||||
};
|
||||
|
||||
async function updateDDNS() {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${CONFIG.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Get current Public IP
|
||||
const ipRes = await fetch('https://api.ipify.org');
|
||||
const currentIp = await ipRes.text();
|
||||
console.log(`[${new Date().toISOString()}] Current IP: ${currentIp}`);
|
||||
|
||||
// 2. Get Record ID
|
||||
const dnsRes = await fetch(`https://api.cloudflare.com/client/v4/zones/${CONFIG.zoneId}/dns_records?name=${CONFIG.recordName}`, { headers });
|
||||
const dnsData = await dnsRes.json();
|
||||
|
||||
if (!dnsData.success) throw new Error(JSON.stringify(dnsData.errors));
|
||||
|
||||
const record = dnsData.result[0];
|
||||
if (record.content === currentIp) {
|
||||
console.log('✅ IP is already up to date.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Update IP
|
||||
const updateRes = await fetch(`https://api.cloudflare.com/client/v4/zones/${CONFIG.zoneId}/dns_records/${record.id}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
type: 'A',
|
||||
name: CONFIG.recordName,
|
||||
content: currentIp,
|
||||
ttl: 1,
|
||||
proxied: CONFIG.proxied
|
||||
})
|
||||
});
|
||||
|
||||
const updateData = await updateRes.json();
|
||||
if (updateData.success) {
|
||||
console.log(`🚀 Updated: ${record.content} -> ${currentIp}`);
|
||||
} else {
|
||||
console.error('❌ Update failed', updateData.errors);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateDDNS();
|
||||
@@ -1,41 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const componentsDir = 'apps/ssr/src/components';
|
||||
const files = ['Navbar.tsx', 'Footer.tsx', 'CartDrawer.tsx', 'ProductCard.tsx'];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(componentsDir, file);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Add "use client" if needed
|
||||
if (['Navbar.tsx', 'CartDrawer.tsx'].includes(file)) {
|
||||
if (!content.startsWith('"use client"')) {
|
||||
content = '"use client";\n' + content;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace imports
|
||||
content = content.replace(/import \{.*?Link.*?\} from 'react-router-dom'/g, "import Link from 'next/link'");
|
||||
if (file === 'Navbar.tsx') {
|
||||
content = content.replace("import Link from 'next/link'", "import Link from 'next/link'\nimport { useRouter, usePathname } from 'next/navigation'");
|
||||
content = content.replace(/const navigate = useNavigate\(\)/g, 'const router = useRouter()\n const pathname = usePathname()');
|
||||
content = content.replace(/navigate\(/g, 'router.push(');
|
||||
}
|
||||
|
||||
// Replace to= with href=
|
||||
content = content.replace(/<Link([^>]*)to=/g, '<Link$1href=');
|
||||
content = content.replace(/<NavLink([^>]*)to=/g, '<Link$1href=');
|
||||
content = content.replace(/<\/NavLink>/g, '</Link>');
|
||||
|
||||
// In Navbar, we need to fix NavLink className logic
|
||||
if (file === 'Navbar.tsx') {
|
||||
content = content.replace(/className=\{navLinkClass\}/g, 'className={navLinkClass}'); // Will fix manually
|
||||
content = content.replace(/className=\{\(\{ isActive \}\) =>/g, 'className={() =>'); // Will fix manually
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`Updated ${file}`);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = 'apps/website/src/components/dynamic/DynamicPageRenderer.tsx';
|
||||
let content = fs.readFileSync(path, 'utf8');
|
||||
|
||||
content = content.replace(/className="ui-section-dark-card group/g, 'className="ui-card group');
|
||||
content = content.replace(/className="ui-section-dark-card-title mt-6 text-xl"/g, 'className="text-xl font-bold text-text-main mt-6"');
|
||||
content = content.replace(/className="ui-section-dark-card-copy mt-3 text-base"/g, 'className="text-base text-muted-text mt-3 whitespace-pre-wrap"');
|
||||
|
||||
fs.writeFileSync(path, content);
|
||||
@@ -1,38 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
// Replace import ... from '../../lib/...' to import ... from '../../../lib/...'
|
||||
// Wait, the files were moved 1 level deeper. So any import starting with '../' needs an extra '../'
|
||||
// To be safe, let's just find and replace `from '../` with `from '../../` etc?
|
||||
// Actually, converting to `@/` is safer:
|
||||
// from '../../components/ => from '@/components/
|
||||
// from '../../../components/ => from '@/components/
|
||||
// from '../../lib/ => from '@/lib/
|
||||
// from '../../../lib/ => from '@/lib/
|
||||
// from '../components/ => from '@/components/
|
||||
// from '../lib/ => from '@/lib/
|
||||
|
||||
const newContent = content.replace(/from '(\.\.\/)+components\//g, "from '@/components/")
|
||||
.replace(/from '(\.\.\/)+lib\//g, "from '@/lib/")
|
||||
.replace(/from '(\.\.\/)+store\//g, "from '@/store/")
|
||||
.replace(/from '(\.\.\/)+hooks\//g, "from '@/hooks/");
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const newContent = content.replace(/import Link from 'next\/link'/g, "import Link from '@/components/LocalizedLink'");
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed link in', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
if (fs.statSync(dirPath).isDirectory()) {
|
||||
walkDir(dirPath, callback);
|
||||
} else {
|
||||
callback(dirPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (!filePath.endsWith('.tsx') && !filePath.endsWith('.ts')) return;
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
// 1. Convert `export const metadata = { title: '...', description: '...' }` to generateMetadata
|
||||
// We need to parse:
|
||||
// export const metadata = {
|
||||
// title: '...',
|
||||
// description: '...',
|
||||
// }
|
||||
|
||||
const metadataRegex = /export const metadata\s*(?::\s*Metadata\s*)?=\s*\{([\s\S]*?)\};?/g;
|
||||
|
||||
// Exclude layout.tsx because we'll keep its global metadata, and handle it separately.
|
||||
if (filePath.endsWith('layout.tsx')) return;
|
||||
|
||||
content = content.replace(metadataRegex, (match, body) => {
|
||||
changed = true;
|
||||
|
||||
// Extract title and description
|
||||
const titleMatch = body.match(/title:\s*['"`](.*?)['"`]/);
|
||||
const descMatch = body.match(/description:\s*['"`](.*?)['"`]/);
|
||||
|
||||
const title = titleMatch ? titleMatch[1] : '';
|
||||
const desc = descMatch ? descMatch[1] : '';
|
||||
|
||||
// Determine the route path (e.g. /shop, /about, /checkout)
|
||||
// from the filePath. e.g. apps/website/src/app/[lang]/shop/page.tsx -> /shop
|
||||
let routePath = '';
|
||||
const relPath = filePath.split('app/[lang]/')[1].replace('/page.tsx', '').replace('page.tsx', '');
|
||||
if (relPath && !relPath.includes('[') && !relPath.includes(']')) {
|
||||
routePath = '/' + relPath;
|
||||
} else if (relPath.includes('[') && relPath.includes(']')) {
|
||||
// Dynamic route, can't just use string replacement easily. We will leave it to be manually fixed or use params.
|
||||
// But wait! If it's a dynamic route, it probably already has `generateMetadata`!
|
||||
// Let's check if there are static metadata in dynamic routes.
|
||||
// Wait, `/order/[id]/page.tsx` has `generateMetadata`.
|
||||
}
|
||||
|
||||
if (relPath.includes('[')) {
|
||||
// Just fallback to empty for dynamic segments if any static metadata exists
|
||||
routePath = '';
|
||||
}
|
||||
|
||||
return `export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
return generateSeoMetadata({
|
||||
title: '${title}',
|
||||
description: '${desc}',
|
||||
path: '${routePath}',
|
||||
lang,
|
||||
});
|
||||
}`;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
if (!content.includes('generateSeoMetadata')) {
|
||||
// Insert import at the top
|
||||
content = `import { generateSeoMetadata } from '@/lib/seo';\n` + content;
|
||||
}
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log('Converted static metadata in', filePath);
|
||||
}
|
||||
});
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
import re
|
||||
|
||||
filePath = 'apps/backend/src/routes/pages.ts'
|
||||
with open(filePath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Update imports
|
||||
old_imports = """import {
|
||||
LocalizedStringSchema,
|
||||
HeroSectionPayloadSchema,
|
||||
CategoriesSectionPayloadSchema,
|
||||
ProductsSectionPayloadSchema,
|
||||
BannerSectionPayloadSchema,
|
||||
TextSectionPayloadSchema,
|
||||
AnnouncementBarSectionPayloadSchema,
|
||||
NewsletterSectionPayloadSchema,
|
||||
EditorialTileSectionPayloadSchema,
|
||||
EditorialCarouselSectionPayloadSchema,
|
||||
HeroArticlePayloadSchema,
|
||||
RichTextPayloadSchema,
|
||||
ScentNotesBreakdownPayloadSchema,
|
||||
QuoteCalloutPayloadSchema,
|
||||
ProductCarouselPayloadSchema,
|
||||
AccordionSectionPayloadSchema,
|
||||
ContactInfoSectionPayloadSchema,
|
||||
ContactFormSectionPayloadSchema
|
||||
} from "@homaroma/types";"""
|
||||
|
||||
new_imports = """import {
|
||||
LocalizedStringSchema,
|
||||
HeroSectionPayloadSchema,
|
||||
CategoriesSectionPayloadSchema,
|
||||
ProductsSectionPayloadSchema,
|
||||
BannerSectionPayloadSchema,
|
||||
TextSectionPayloadSchema,
|
||||
AnnouncementBarSectionPayloadSchema,
|
||||
NewsletterSectionPayloadSchema,
|
||||
EditorialTileSectionPayloadSchema,
|
||||
EditorialCarouselSectionPayloadSchema,
|
||||
HeroArticleSectionPayloadSchema,
|
||||
RichTextSectionPayloadSchema,
|
||||
ScentNotesBreakdownSectionPayloadSchema,
|
||||
QuoteCalloutSectionPayloadSchema,
|
||||
ProductCarouselSectionPayloadSchema,
|
||||
AccordionSectionPayloadSchema,
|
||||
ContactInfoSectionPayloadSchema,
|
||||
ContactFormSectionPayloadSchema
|
||||
} from "@homaroma/types";
|
||||
import { z } from "zod";"""
|
||||
|
||||
content = content.replace(old_imports, new_imports)
|
||||
|
||||
# 2. Remove manual validation functions
|
||||
# From 'function isLocalizedStringValue' up to 'const RESERVED_SLUGS'
|
||||
start_idx = content.find('function isLocalizedStringValue')
|
||||
end_idx = content.find('const RESERVED_SLUGS')
|
||||
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
content = content[:start_idx] + content[end_idx:]
|
||||
|
||||
# 3. Refactor validateSectionPayload
|
||||
old_validate_func = """function validateSectionPayload(type: string, payload: any): string | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return "Payload must be a JSON object";
|
||||
}
|
||||
|
||||
switch (type.toUpperCase()) {
|
||||
case 'HERO': {
|
||||
const res = HeroSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'CATEGORIES': {
|
||||
const res = CategoriesSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'PRODUCTS': {
|
||||
const res = ProductsSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'BANNER': {
|
||||
const res = BannerSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'TEXT': {
|
||||
const res = TextSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'ANNOUNCEMENT_BAR': {
|
||||
const res = AnnouncementBarSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'NEWSLETTER': {
|
||||
const res = NewsletterSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'EDITORIAL_TILE': {
|
||||
const res = EditorialTileSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'EDITORIAL_CAROUSEL': {
|
||||
const res = EditorialCarouselSectionPayloadSchema.safeParse(payload);
|
||||
if (!res.success) return res.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
break;
|
||||
}
|
||||
case 'HERO_ARTICLE': {
|
||||
const error = validateHeroArticlePayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'RICH_TEXT': {
|
||||
const error = validateRichTextPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'SCENT_NOTES_BREAKDOWN': {
|
||||
const error = validateScentNotesBreakdownPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'QUOTE_CALLOUT': {
|
||||
const error = validateQuoteCalloutPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'PRODUCT_CAROUSEL': {
|
||||
const error = validateProductCarouselPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'ACCORDION': {
|
||||
const error = validateAccordionPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'CONTACT_INFO': {
|
||||
const error = validateContactInfoPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
case 'CONTACT_FORM': {
|
||||
const error = validateContactFormPayload(payload);
|
||||
if (error) return error;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return `Unknown section type: ${type}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}"""
|
||||
|
||||
new_validate_func = """function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] as z.ZodIssue[] };
|
||||
}
|
||||
|
||||
let schema: z.ZodTypeAny;
|
||||
|
||||
switch (type.toUpperCase()) {
|
||||
case 'HERO': schema = HeroSectionPayloadSchema; break;
|
||||
case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break;
|
||||
case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break;
|
||||
case 'BANNER': schema = BannerSectionPayloadSchema; break;
|
||||
case 'TEXT': schema = TextSectionPayloadSchema; break;
|
||||
case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break;
|
||||
case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break;
|
||||
case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break;
|
||||
case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break;
|
||||
case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break;
|
||||
case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break;
|
||||
case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break;
|
||||
case 'ACCORDION': schema = AccordionSectionPayloadSchema; break;
|
||||
case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break;
|
||||
case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break;
|
||||
default:
|
||||
return { success: false, issues: [{ code: 'custom', message: `Unknown section type: ${type}`, path: [] }] as z.ZodIssue[] };
|
||||
}
|
||||
|
||||
const res = schema.safeParse(payload);
|
||||
if (!res.success) {
|
||||
return { success: false, issues: res.error.issues };
|
||||
}
|
||||
return { success: true };
|
||||
}"""
|
||||
|
||||
# I need to use re.sub for validateSectionPayload in case of slight whitespace differences, but a simple find+replace is safer if exact.
|
||||
# Let's just use re.sub with a regex that captures everything from `function validateSectionPayload(type: string, payload: any): string | null {` to the end of the file.
|
||||
pattern = r'function validateSectionPayload\(type: string, payload: any\): string \| null \{[\s\S]*'
|
||||
content = re.sub(pattern, new_validate_func, content)
|
||||
|
||||
|
||||
# 4. Update error handling in POST and PUT
|
||||
# POST
|
||||
old_post_err = """ const validationError = validateSectionPayload(type, payload);
|
||||
if (validationError) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
}"""
|
||||
new_post_err = """ const validationResult = validateSectionPayload(type, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}"""
|
||||
content = content.replace(old_post_err, new_post_err)
|
||||
|
||||
# PUT
|
||||
old_put_err = """ const validationError = validateSectionPayload(validationType, payload);
|
||||
if (validationError) {
|
||||
set.status = 400;
|
||||
return { error: validationError };
|
||||
}"""
|
||||
new_put_err = """ const validationResult = validateSectionPayload(validationType, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}"""
|
||||
content = content.replace(old_put_err, new_put_err)
|
||||
|
||||
with open(filePath, 'w') as f:
|
||||
f.write(content)
|
||||
@@ -1,30 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function walkDir(dir, callback) {
|
||||
fs.readdirSync(dir).forEach(f => {
|
||||
let dirPath = path.join(dir, f);
|
||||
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||
isDirectory ? walkDir(dirPath, callback) : callback(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||
if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
let newContent = content.replace(/\/\/ lang is extracted from paramsexport/g, "// lang is extracted from params\nexport")
|
||||
.replace(/\/\/ lang is extracted from paramsfunction/g, "// lang is extracted from params\nfunction")
|
||||
.replace(/\/\/ lang is extracted from paramstype/g, "// lang is extracted from params\ntype")
|
||||
.replace(/\/\/ lang is extracted from paramsconst/g, "// lang is extracted from params\nconst");
|
||||
|
||||
if (filePath.endsWith('layout.tsx')) {
|
||||
newContent = newContent.replace("import './globals.css';", "import '../globals.css';");
|
||||
}
|
||||
|
||||
if (newContent !== content) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf8');
|
||||
console.log('Fixed syntax in', filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const files = [
|
||||
'apps/spa/src/pages/AboutPage.tsx',
|
||||
'apps/spa/src/pages/ContactPage.tsx',
|
||||
'apps/spa/src/pages/FaqPage.tsx',
|
||||
'apps/spa/src/pages/ScentGuidePage.tsx',
|
||||
'apps/spa/src/pages/NotFoundPage.tsx',
|
||||
'apps/spa/src/pages/ArticleReaderPage.tsx'
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// Specific replaces for safety
|
||||
content = content.replace(/bg-night-950/g, 'bg-pop-bg');
|
||||
content = content.replace(/bg-night-900/g, 'bg-white');
|
||||
content = content.replace(/bg-night-800\/20/g, 'bg-white border-b border-spa-stone/10');
|
||||
content = content.replace(/bg-night-[0-9]+\/[0-9]+/g, 'bg-spa-stone/10');
|
||||
|
||||
content = content.replace(/text-cyan-glow/g, 'text-spa-forest');
|
||||
content = content.replace(/bg-cyan-glow\/[0-9]+/g, 'bg-spa-forest/10');
|
||||
content = content.replace(/bg-cyan-glow/g, 'bg-spa-forest text-white');
|
||||
|
||||
content = content.replace(/border-cyan-glow\/[0-9]+/g, 'border-spa-forest');
|
||||
content = content.replace(/border-white\/[0-9]+/g, 'border-spa-stone/10');
|
||||
|
||||
content = content.replace(/text-night-900/g, 'text-spa-forest');
|
||||
content = content.replace(/to-night-700/g, 'to-spa-stone');
|
||||
|
||||
// Replace glass with light theme equivalent
|
||||
content = content.replace(/glass/g, 'bg-white shadow-sm border border-spa-stone/10');
|
||||
|
||||
fs.writeFileSync(file, content);
|
||||
}
|
||||
console.log("Done fixing themes.");
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html lang="th" class="sarabun_388a5c3-module__3SHriq__variable kanit_f5fedee1-module__EPOHma__variable scroll-smooth"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/chunks/%5Broot-of-the-server%5D__0biig8i._.css" data-precedence="next_static/chunks/[root-of-the-server]__0biig8i._.css"/><link rel="stylesheet" href="/_next/static/chunks/07-k_swiper_swiper_00i-jcc.css" data-precedence="next_static/chunks/07-k_swiper_swiper_00i-jcc.css"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/%5Bturbopack%5D_browser_dev_hmr-client_hmr-client_ts_0wters9._.js"/><script src="/_next/static/chunks/0-d2_next_dist_compiled_next-devtools_index_0095.qm.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_compiled_react-dom_04f_5_f._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_compiled_react-server-dom-turbopack_0ssyuat._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_compiled_0p9w5cq._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_client_0uxyif6._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_0tzo9q~._.js" async=""></script><script src="/_next/static/chunks/0~ve_%40swc_helpers_cjs_097dtp9._.js" async=""></script><script src="/_next/static/chunks/apps_website_0rqeker._.js" async=""></script><script src="/_next/static/chunks/turbopack-apps_website_0y.p17v._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_0ocmezb._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_client_components_builtin_not-found_03y66kz.js" async=""></script><script src="/_next/static/chunks/0-d2_next_dist_client_components_builtin_global-error_03y66kz.js" async=""></script><script src="/_next/static/chunks/_06n5idi._.js" async=""></script><script src="/_next/static/chunks/0-d2_next_0ik_n4d._.js" async=""></script><script src="/_next/static/chunks/0nx-_zod_v4_0lohmlh._.js" async=""></script><script src="/_next/static/chunks/node_modules__bun_0u-jy1s._.js" async=""></script><script src="/_next/static/chunks/apps_website_src_app_%5Blang%5D_layout_tsx_03y66kz._.js" async=""></script><script src="/_next/static/chunks/apps_website_src_components_08sg2.6._.js" async=""></script><script src="/_next/static/chunks/node_modules__bun_0u-0qmu._.js" async=""></script><script src="/_next/static/chunks/apps_website_src_app_%5Blang%5D_%5Bslug%5D_page_tsx_0085uc7._.js" async=""></script><meta name="next-size-adjust" content=""/><title>Mega 01 | HomAroma</title><meta name="description" content="Mega 01"/><link rel="canonical" href="http://localhost:3000/th/mega01"/><link rel="alternate" hrefLang="th" href="http://localhost:3000/th/mega01"/><link rel="alternate" hrefLang="en" href="http://localhost:3000/en/mega01"/><link rel="alternate" hrefLang="ja" href="http://localhost:3000/ja/mega01"/><link rel="alternate" hrefLang="zh" href="http://localhost:3000/zh/mega01"/><link rel="alternate" hrefLang="vi" href="http://localhost:3000/vi/mega01"/><link rel="alternate" hrefLang="ko" href="http://localhost:3000/ko/mega01"/><link rel="icon" href="/favicon.ico?favicon.0x3dzn~oxb6tn.ico" sizes="256x256" type="image/x-icon"/><script src="/_next/static/chunks/0-d2_next_dist_build_polyfills_polyfill-nomodule.js" noModule=""></script></head><body class="min-h-screen bg-surface-page text-text-main font-sans antialiased" style="--color-brand-primary:#FF6B6B;--color-brand-primary-hover:#FF4500;--color-brand-accent:#FFF200;--color-brand-warm:#FFA07A;--color-brand-cool:#E0FFFF;--color-brand-success:#98FB98;--color-brand-soft:#D8BFD8;--color-surface-page:#FFF0F5;--color-surface-nav-rest:#F9F8F5;--color-surface-card:#ffffff;--color-text-main:#333333;--color-muted-text:#666666;--color-line-primary:#00B900;--color-line-primary-hover:#009900;--shadow-card:0 10px 25px rgba(0,0,0,0.05);--shadow-card-hover:0 20px 40px rgba(0,0,0,0.1)"><div hidden=""><!--$--><!--/$--></div><nav data-scrolled="false" class="global-navbar fixed top-0 z-50 w-full border-b border-black/5 px-4 py-3.5 transition-all duration-300 md:px-6 md:py-4 bg-surface-page/80 backdrop-blur-xl md:bg-surface-nav-rest md:backdrop-blur-none"><div class="flex w-full items-center justify-between gap-4"><div class="flex shrink-0 items-center"><a class="ui-brand-wordmark" href="/th">HomAroma</a></div><div class="flex flex-1 justify-center"><div class="hidden space-x-7 text-sm font-medium uppercase tracking-wide md:flex"><div class="relative"><a class="flex items-center gap-1 border-b py-2 transition-colors border-transparent text-text-main hover:border-brand-primary hover:text-text-main" href="/th">ข่าวสาร</a></div><div class="relative"><a class="flex items-center gap-1 border-b py-2 transition-colors border-transparent text-text-main hover:border-brand-primary hover:text-text-main" href="/th/shop">ซื้อสินค้า<svg class=Line truncated
|
||||
@@ -1,31 +0,0 @@
|
||||
const cheerio = require('cheerio');
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('mega01.html', 'utf8');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
console.log('--- Page Title ---');
|
||||
console.log($('title').text());
|
||||
|
||||
console.log('\n--- Main Content Sections ---');
|
||||
const sections = [];
|
||||
$('section, .section, [data-section]').each((i, el) => {
|
||||
sections.push({
|
||||
index: i + 1,
|
||||
id: $(el).attr('id') || 'no-id',
|
||||
class: $(el).attr('class') || 'no-class',
|
||||
text: $(el).text().replace(/\s+/g, ' ').substring(0, 500)
|
||||
});
|
||||
});
|
||||
if (sections.length === 0) {
|
||||
// If no semantic tags, maybe just print divs with specific classes
|
||||
$('div').each((i, el) => {
|
||||
if($(el).attr('class') && $(el).attr('class').includes('py-')) {
|
||||
sections.push({
|
||||
index: i + 1,
|
||||
class: $(el).attr('class') || 'no-class',
|
||||
text: $(el).text().replace(/\s+/g, ' ').substring(0, 500)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(JSON.stringify(sections, null, 2));
|
||||
@@ -1,21 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const adminFiles = [
|
||||
'apps/admin/src/pages/ProductsPage.tsx',
|
||||
'apps/admin/src/pages/ProductEditPage.tsx',
|
||||
'apps/admin/src/components/page-builder/ProductsForm.tsx',
|
||||
'apps/admin/src/pages/PageBuilderPage.tsx',
|
||||
'apps/admin/src/pages/DashboardPage.tsx'
|
||||
];
|
||||
|
||||
adminFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
// We only replace scent variables related to product.scent -> product.brand
|
||||
// and /products/scents -> /products/brands
|
||||
content = content.replace(/\b([Ss])cent\b/g, (match, p1) => p1 === 'S' ? 'Brand' : 'brand');
|
||||
content = content.replace(/\b([Ss])cents\b/g, (match, p1) => p1 === 'S' ? 'Brands' : 'brands');
|
||||
content = content.replace(/\bscentId\b/g, 'brandId');
|
||||
fs.writeFileSync(file, content);
|
||||
}
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import Link from './LocalizedLink'
|
||||
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
||||
import { useLang } from '../hooks/useLang'
|
||||
|
||||
interface Props {
|
||||
product: Product
|
||||
}
|
||||
|
||||
export default function AddToCartSection({ product }: Props) {
|
||||
const [selectedOptions, setSelectedOptions] = useState<string[]>(() => {
|
||||
if (product.variants && product.variants.length > 0) {
|
||||
const first = product.variants[0];
|
||||
return [first.option1 || '', first.option2 || '', first.option3 || ''];
|
||||
}
|
||||
return ['', '', ''];
|
||||
});
|
||||
|
||||
const { addToCart, toggleCart } = useCartStore()
|
||||
const lang = useLang()
|
||||
|
||||
const currentVariant = useMemo(() => {
|
||||
if (!product.variants) return undefined;
|
||||
return product.variants.find(v => {
|
||||
const v1 = v.option1 || '';
|
||||
const v2 = v.option2 || '';
|
||||
const v3 = v.option3 || '';
|
||||
const s1 = selectedOptions[0] || '';
|
||||
const s2 = selectedOptions[1] || '';
|
||||
const s3 = selectedOptions[2] || '';
|
||||
return v1 === s1 && v2 === s2 && v3 === s3;
|
||||
});
|
||||
}, [product.variants, selectedOptions]);
|
||||
|
||||
const categoryName = resolveCategoryName(product.category, lang)
|
||||
const productName = resolveLocalized(product.name, lang) || product.brand.name
|
||||
const description = resolveLocalized(product.description, lang)
|
||||
|
||||
const isOptionAvailable = (optionIndex: number, value: string) => {
|
||||
if (!product.variants) return false;
|
||||
return product.variants.some(v => {
|
||||
const vOpts = [v.option1 || '', v.option2 || '', v.option3 || ''];
|
||||
if (vOpts[optionIndex] !== value) return false;
|
||||
|
||||
// Check if it matches currently selected options for OTHER indices
|
||||
for (let i = 0; i < (product.options?.length || 0); i++) {
|
||||
if (i === optionIndex) continue;
|
||||
if (vOpts[i] !== (selectedOptions[i] || '')) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleOptionChange = (optIndex: number, val: string) => {
|
||||
setSelectedOptions(prev => {
|
||||
const next = [...prev];
|
||||
next[optIndex] = val;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!currentVariant) return
|
||||
|
||||
const variantName = [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ');
|
||||
|
||||
addToCart({
|
||||
productId: product.id,
|
||||
variantId: currentVariant.id,
|
||||
quantity: 1,
|
||||
price: currentVariant.price || product.price || 0,
|
||||
productName,
|
||||
variantSize: variantName || undefined,
|
||||
productImage: product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName),
|
||||
})
|
||||
toggleCart(true)
|
||||
}
|
||||
|
||||
const currentVariantName = currentVariant ? [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ') : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-2">
|
||||
{categoryName && (
|
||||
<p className="ui-eyebrow">{categoryName}</p>
|
||||
)}
|
||||
<h1 className="text-4xl font-semibold leading-tight text-text-main md:text-6xl">
|
||||
{productName}
|
||||
</h1>
|
||||
<p className="text-sm font-semibold uppercase tracking-widest text-muted-text">
|
||||
Premium Collection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{description && <p className="leading-relaxed text-muted-text">{description}</p>}
|
||||
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-4xl font-bold text-brand-primary">
|
||||
{formatPrice(currentVariant?.price ?? product.price)}
|
||||
</span>
|
||||
{currentVariantName && <span className="text-sm text-muted-text">/ {currentVariantName}</span>}
|
||||
</div>
|
||||
|
||||
{product.options && product.options.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
{product.options.map((option, optIndex) => (
|
||||
<div key={option.name} className="space-y-3">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-muted-text">Select {option.name}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{option.values.map(val => {
|
||||
const isSelected = selectedOptions[optIndex] === val;
|
||||
const isAvailable = isOptionAvailable(optIndex, val);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={val}
|
||||
disabled={!isAvailable}
|
||||
onClick={() => handleOptionChange(optIndex, val)}
|
||||
className={`rounded-xl border px-5 py-3 text-sm font-bold uppercase tracking-widest transition ${
|
||||
isSelected
|
||||
? 'border-brand-primary bg-brand-primary text-surface-card shadow-card'
|
||||
: isAvailable
|
||||
? 'border-line-primary/20 bg-surface-card text-muted-text hover:border-brand-primary hover:text-brand-primary'
|
||||
: 'border-line-primary/10 bg-surface-card/50 text-muted-text/30 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{val}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hidden gap-4 md:flex">
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={!currentVariant}
|
||||
className={`ui-form-inline-button flex-1 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{currentVariant ? 'Add to Cart' : 'Unavailable'}
|
||||
</button>
|
||||
{categoryName && (
|
||||
<Link
|
||||
href={`/category/${product.category.id}`}
|
||||
className="rounded-full border border-line-primary/20 bg-surface-card px-8 py-4 font-bold uppercase tracking-widest text-muted-text transition hover:border-brand-primary hover:text-brand-primary"
|
||||
>
|
||||
{categoryName}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 border-t border-line-primary/20 pt-6 text-sm text-muted-text">
|
||||
<div>
|
||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-muted-text/70">Brand</span>
|
||||
<span className="font-semibold text-text-main">{product.brand.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-muted-text/70">Category</span>
|
||||
<Link href={`/category/${product.category.id}`} className="font-semibold text-text-main transition hover:text-brand-primary">
|
||||
{categoryName}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 z-40 border-t border-line-primary/20 bg-surface-card/95 p-4 shadow-[0_-10px_30px_rgba(0,0,0,0.08)] backdrop-blur md:hidden">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-text/70">
|
||||
{currentVariantName || 'Total Price'}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-brand-primary">{formatPrice(currentVariant?.price ?? product.price)}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={!currentVariant}
|
||||
className={`ui-form-inline-button flex-1 px-5 py-3.5 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{currentVariant ? 'Add to Cart' : 'Unavailable'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
const tree = [{ nodeId: "root1" }, { nodeId: "root2", links: [] }];
|
||||
function findNodeAndParent(tree: any[], targetId: string, parent: any = null, list: any[] = tree): any {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
if (tree[i].nodeId === targetId) return { node: tree[i], parent, list, index: i };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
console.log(findNodeAndParent(tree, "root1").parent === null);
|
||||
@@ -1,89 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, 'apps/backend/src/routes/pages.ts');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
content = content.replace(/import \{[\s\S]*?\} from "@homaroma\/types";/, `import {
|
||||
LocalizedStringSchema,
|
||||
HeroSectionPayloadSchema,
|
||||
CategoriesSectionPayloadSchema,
|
||||
ProductsSectionPayloadSchema,
|
||||
BannerSectionPayloadSchema,
|
||||
TextSectionPayloadSchema,
|
||||
AnnouncementBarSectionPayloadSchema,
|
||||
NewsletterSectionPayloadSchema,
|
||||
EditorialTileSectionPayloadSchema,
|
||||
EditorialCarouselSectionPayloadSchema,
|
||||
HeroArticleSectionPayloadSchema,
|
||||
RichTextSectionPayloadSchema,
|
||||
ScentNotesBreakdownSectionPayloadSchema,
|
||||
QuoteCalloutSectionPayloadSchema,
|
||||
ProductCarouselSectionPayloadSchema,
|
||||
AccordionSectionPayloadSchema,
|
||||
ContactInfoSectionPayloadSchema,
|
||||
ContactFormSectionPayloadSchema
|
||||
} from "@homaroma/types";
|
||||
import { z } from "zod";`);
|
||||
|
||||
// Remove manual validation functions
|
||||
content = content.replace(/function isLocalizedStringValue[\s\S]*?function validateContactFormPayload[^{]*\{[\s\S]*?return null;\n\}/, '');
|
||||
|
||||
// Refactor validateSectionPayload
|
||||
content = content.replace(/function validateSectionPayload\(type: string, payload: any\): string \| null \{[\s\S]*?return null;\n\}/, `function validateSectionPayload(type: string, payload: any): { success: boolean; issues?: z.ZodIssue[] } {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return { success: false, issues: [{ code: 'custom', message: "Payload must be a JSON object", path: [] }] };
|
||||
}
|
||||
|
||||
let schema: z.ZodTypeAny;
|
||||
|
||||
switch (type.toUpperCase()) {
|
||||
case 'HERO': schema = HeroSectionPayloadSchema; break;
|
||||
case 'CATEGORIES': schema = CategoriesSectionPayloadSchema; break;
|
||||
case 'PRODUCTS': schema = ProductsSectionPayloadSchema; break;
|
||||
case 'BANNER': schema = BannerSectionPayloadSchema; break;
|
||||
case 'TEXT': schema = TextSectionPayloadSchema; break;
|
||||
case 'ANNOUNCEMENT_BAR': schema = AnnouncementBarSectionPayloadSchema; break;
|
||||
case 'NEWSLETTER': schema = NewsletterSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_TILE': schema = EditorialTileSectionPayloadSchema; break;
|
||||
case 'EDITORIAL_CAROUSEL': schema = EditorialCarouselSectionPayloadSchema; break;
|
||||
case 'HERO_ARTICLE': schema = HeroArticleSectionPayloadSchema; break;
|
||||
case 'RICH_TEXT': schema = RichTextSectionPayloadSchema; break;
|
||||
case 'SCENT_NOTES_BREAKDOWN': schema = ScentNotesBreakdownSectionPayloadSchema; break;
|
||||
case 'QUOTE_CALLOUT': schema = QuoteCalloutSectionPayloadSchema; break;
|
||||
case 'PRODUCT_CAROUSEL': schema = ProductCarouselSectionPayloadSchema; break;
|
||||
case 'ACCORDION': schema = AccordionSectionPayloadSchema; break;
|
||||
case 'CONTACT_INFO': schema = ContactInfoSectionPayloadSchema; break;
|
||||
case 'CONTACT_FORM': schema = ContactFormSectionPayloadSchema; break;
|
||||
default:
|
||||
return { success: false, issues: [{ code: 'custom', message: \`Unknown section type: \${type}\`, path: [] }] };
|
||||
}
|
||||
|
||||
const res = schema.safeParse(payload);
|
||||
if (!res.success) {
|
||||
return { success: false, issues: res.error.issues };
|
||||
}
|
||||
return { success: true };
|
||||
}`);
|
||||
|
||||
// Update error handling in POST /:slug/sections
|
||||
content = content.replace(
|
||||
/const validationError = validateSectionPayload\(type, payload\);\n\s*if \(validationError\) \{\n\s*set\.status = 400;\n\s*return \{ error: validationError \};\n\s*\}/g,
|
||||
`const validationResult = validateSectionPayload(type, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}`
|
||||
);
|
||||
|
||||
// Update error handling in PUT /sections/:sectionId
|
||||
content = content.replace(
|
||||
/const validationError = validateSectionPayload\(validationType, payload\);\n\s*if \(validationError\) \{\n\s*set\.status = 400;\n\s*return \{ error: validationError \};\n\s*\}/g,
|
||||
`const validationResult = validateSectionPayload(validationType, payload);
|
||||
if (!validationResult.success) {
|
||||
set.status = 400;
|
||||
return { error: "Validation failed", details: validationResult.issues };
|
||||
}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
Reference in new issue
Block a user