This commit is contained in:
Your Name committed 2026-09-29 07:40:08 +07:00
1 parent 602eaa9260
commit c42dd4272f
30 files changed
+2024 -252

No files matched your search

@@ -0,0 +1,43 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "cashierId" INTEGER,
ADD COLUMN "changeAmount" DOUBLE PRECISION,
ADD COLUMN "channel" TEXT NOT NULL DEFAULT 'ONLINE',
ADD COLUMN "paymentMethod" TEXT DEFAULT 'CASH',
ADD COLUMN "receivedAmount" DOUBLE PRECISION,
ALTER COLUMN "customerName" DROP NOT NULL,
ALTER COLUMN "customerPhone" DROP NOT NULL,
ALTER COLUMN "customerEmail" DROP NOT NULL,
ALTER COLUMN "shippingAddress" DROP NOT NULL,
ALTER COLUMN "shippingFee" SET DEFAULT 0;
-- AlterTable
ALTER TABLE "Page" ADD COLUMN "ogImageUrl" TEXT,
ADD COLUMN "seoDescription" TEXT,
ADD COLUMN "seoKeywords" TEXT,
ADD COLUMN "seoTitle" TEXT;
-- AlterTable
ALTER TABLE "PageTranslation" ADD COLUMN "seoDescription" TEXT,
ADD COLUMN "seoKeywords" TEXT,
ADD COLUMN "seoTitle" TEXT;
-- AlterTable
ALTER TABLE "StoreSettings" ADD COLUMN "defaultOgImage" TEXT,
ADD COLUMN "defaultSeoDescription" TEXT,
ADD COLUMN "defaultSeoTitle" TEXT;
-- AlterTable
ALTER TABLE "StoreSettingsTranslation" ADD COLUMN "defaultSeoDescription" TEXT,
ADD COLUMN "defaultSeoTitle" TEXT;
-- CreateIndex
CREATE INDEX "Order_channel_idx" ON "Order"("channel");
-- CreateIndex
CREATE INDEX "Order_createdAt_idx" ON "Order"("createdAt");
-- CreateIndex
CREATE INDEX "TiktokSettlement_orderSettledTime_idx" ON "TiktokSettlement"("orderSettledTime");
-- CreateIndex
CREATE INDEX "TiktokSettlement_orderId_idx" ON "TiktokSettlement"("orderId");
+27 -15
View File
@@ -285,6 +285,9 @@ model StoreSettings {
scrollRevealStyle String @default("fade-up")
scrollRevealDuration Int @default(700)
headerStyle String @default("single_line")
defaultSeoTitle String?
defaultSeoDescription String?
defaultOgImage String?
translations StoreSettingsTranslation[]
updatedAt DateTime @updatedAt
}
@@ -299,6 +302,8 @@ model StoreSettingsTranslation {
newsletterDescription String?
newsletterPlaceholder String?
newsletterButtonText String?
defaultSeoTitle String?
defaultSeoDescription String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -660,24 +665,31 @@ model Review {
}
model Page {
id Int @id @default(autoincrement())
slug String @unique // e.g., "home", "about-us"
title String
sections PageSection[]
translations PageTranslation[]
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
slug String @unique // e.g., "home", "about-us"
title String
seoTitle String?
seoDescription String?
seoKeywords String?
ogImageUrl String?
sections PageSection[]
translations PageTranslation[]
isPublished Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PageTranslation {
id Int @id @default(autoincrement())
pageId Int
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
langCode String
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
pageId Int
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
langCode String
title String
seoTitle String?
seoDescription String?
seoKeywords String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([pageId, langCode])
}
+254 -41
View File
@@ -221,6 +221,100 @@ export function mergeSectionTranslations(payload: any, translations: any, paths:
return { payload: result, translatedPaths };
}
export async function savePageTranslations(pageId: number, payload: any) {
if (!payload || typeof payload !== "object") return [];
const results: any[] = [];
const LANGS = ["th", "en", "ja", "zh", "vi", "ko", "lo", "it"];
const page = await prisma.page.findUnique({ where: { id: pageId } });
if (!page) return results;
if (Array.isArray(payload)) {
for (const item of payload) {
const lang = item.langCode || item.lang;
if (!lang || !LANGS.includes(lang)) continue;
const title = item.title ?? page.title;
const seoTitle = item.seoTitle !== undefined ? item.seoTitle : undefined;
const seoDescription = item.seoDescription !== undefined ? item.seoDescription : undefined;
const seoKeywords = item.seoKeywords !== undefined ? item.seoKeywords : undefined;
const updateData: any = {};
if (item.title !== undefined) updateData.title = item.title;
if (seoTitle !== undefined) updateData.seoTitle = seoTitle;
if (seoDescription !== undefined) updateData.seoDescription = seoDescription;
if (seoKeywords !== undefined) updateData.seoKeywords = seoKeywords;
const updated = await prisma.pageTranslation.upsert({
where: { pageId_langCode: { pageId, langCode: lang } },
update: updateData,
create: {
pageId,
langCode: lang,
title,
seoTitle: seoTitle ?? null,
seoDescription: seoDescription ?? null,
seoKeywords: seoKeywords ?? null,
},
});
results.push(updated);
}
return results;
}
for (const lang of LANGS) {
const langObj = payload[lang];
const title = typeof langObj?.title === "string"
? langObj.title
: typeof payload.title === "string"
? (lang === "th" ? payload.title : undefined)
: payload.title?.[lang];
const seoTitle = typeof langObj?.seoTitle === "string" || langObj?.seoTitle === null
? langObj.seoTitle
: typeof payload.seoTitle === "string" || payload.seoTitle === null
? (lang === "th" ? payload.seoTitle : undefined)
: payload.seoTitle?.[lang];
const seoDescription = typeof langObj?.seoDescription === "string" || langObj?.seoDescription === null
? langObj.seoDescription
: typeof payload.seoDescription === "string" || payload.seoDescription === null
? (lang === "th" ? payload.seoDescription : undefined)
: payload.seoDescription?.[lang];
const seoKeywords = typeof langObj?.seoKeywords === "string" || langObj?.seoKeywords === null
? langObj.seoKeywords
: typeof payload.seoKeywords === "string" || payload.seoKeywords === null
? (lang === "th" ? payload.seoKeywords : undefined)
: payload.seoKeywords?.[lang];
if (title === undefined && seoTitle === undefined && seoDescription === undefined && seoKeywords === undefined) {
continue;
}
const updateData: any = {};
if (title !== undefined) updateData.title = title;
if (seoTitle !== undefined) updateData.seoTitle = seoTitle;
if (seoDescription !== undefined) updateData.seoDescription = seoDescription;
if (seoKeywords !== undefined) updateData.seoKeywords = seoKeywords;
const updated = await prisma.pageTranslation.upsert({
where: { pageId_langCode: { pageId, langCode: lang } },
update: updateData,
create: {
pageId,
langCode: lang,
title: title ?? page.title,
seoTitle: seoTitle ?? null,
seoDescription: seoDescription ?? null,
seoKeywords: seoKeywords ?? null,
},
});
results.push(updated);
}
return results;
}
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async ({ query }) => {
const view = String((query as any)?.view || "summary").toLowerCase();
@@ -230,6 +324,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
sections: {
orderBy: { order: "asc" },
},
translations: true,
},
});
}
@@ -239,6 +334,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
id: true,
slug: true,
title: true,
seoTitle: true,
seoDescription: true,
seoKeywords: true,
ogImageUrl: true,
isPublished: true,
createdAt: true,
updatedAt: true,
@@ -262,6 +361,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
where: includeInactive ? undefined : { isActive: true },
orderBy: { order: "asc" },
},
translations: true,
},
});
if (!page) {
@@ -273,9 +373,22 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
page.sections.map(async (section: any) => await hydrateSectionPayload(section, lang))
);
const trans = page.translations?.find((t: any) => t.langCode === lang);
const title = trans?.title || page.title;
const seoTitle = trans?.seoTitle || page.seoTitle;
const seoDescription = trans?.seoDescription || page.seoDescription;
const seoKeywords = trans?.seoKeywords || page.seoKeywords;
return {
...page,
title,
seoTitle,
seoDescription,
seoKeywords,
ogImageUrl: page.ogImageUrl,
isPublished: page.isPublished,
sections: populatedSections,
translations: page.translations,
};
})
.get("/:slug/sections", async ({ params: { slug }, set }) => {
@@ -545,6 +658,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
title: string;
slug: string;
isPublished?: boolean;
seoTitle?: string | null;
seoDescription?: string | null;
seoKeywords?: string | null;
ogImageUrl?: string | null;
};
section: {
type: string;
@@ -617,6 +734,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
title: pageInput.title,
slug: pageInput.slug,
isPublished: pageInput.isPublished ?? false,
seoTitle: pageInput.seoTitle ?? null,
seoDescription: pageInput.seoDescription ?? null,
seoKeywords: pageInput.seoKeywords ?? null,
ogImageUrl: pageInput.ogImageUrl ?? null,
},
});
const section = await tx.pageSection.create({
@@ -661,6 +782,10 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
title: t.String(),
slug: t.String(),
isPublished: t.Optional(t.Boolean()),
seoTitle: t.Optional(t.Nullable(t.String())),
seoDescription: t.Optional(t.Nullable(t.String())),
seoKeywords: t.Optional(t.Nullable(t.String())),
ogImageUrl: t.Optional(t.Nullable(t.String())),
}),
section: t.Object({
type: t.String(),
@@ -677,7 +802,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
const data = body as any;
const operation = data?.id ? "update" : "create";
try {
const { id, slug, title, isPublished, status, metadata } = data;
const { id, slug, title, isPublished, status, metadata, seoTitle, seoDescription, seoKeywords, ogImageUrl, translations } = data;
if (
typeof slug === "string" &&
@@ -752,14 +877,41 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
}
}
const page = id
? await prisma.page.update({
where: { id: Number(id) },
data: { slug, title, isPublished: finalIsPublished },
})
: await prisma.page.create({
data: { slug, title, isPublished: finalIsPublished },
});
let page: any;
if (id) {
const updateData: any = { slug, title, isPublished: finalIsPublished };
if (seoTitle !== undefined) updateData.seoTitle = seoTitle;
if (seoDescription !== undefined) updateData.seoDescription = seoDescription;
if (seoKeywords !== undefined) updateData.seoKeywords = seoKeywords;
if (ogImageUrl !== undefined) updateData.ogImageUrl = ogImageUrl;
page = await prisma.page.update({
where: { id: Number(id) },
data: updateData,
include: { translations: true },
});
} else {
page = await prisma.page.create({
data: {
slug,
title,
isPublished: finalIsPublished,
seoTitle: seoTitle ?? null,
seoDescription: seoDescription ?? null,
seoKeywords: seoKeywords ?? null,
ogImageUrl: ogImageUrl ?? null,
},
include: { translations: true },
});
}
if (translations) {
await savePageTranslations(page.id, translations);
page = await prisma.page.findUnique({
where: { id: page.id },
include: { translations: true },
});
}
return {
success: true,
@@ -802,6 +954,80 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
slug: t.String(),
title: t.String(),
isPublished: t.Optional(t.Boolean()),
seoTitle: t.Optional(t.Nullable(t.String())),
seoDescription: t.Optional(t.Nullable(t.String())),
seoKeywords: t.Optional(t.Nullable(t.String())),
ogImageUrl: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Any()),
}),
},
)
.put(
"/:id",
async ({ params: { id: pageParam }, body, set }) => {
try {
const numId = Number(pageParam);
const isNumericId = Number.isInteger(numId) && numId > 0;
const existing = await prisma.page.findFirst({
where: isNumericId ? { id: numId } : { slug: pageParam },
});
if (!existing) {
set.status = 404;
return { error: "Page not found" };
}
const data = body as any;
const { slug, title, isPublished, seoTitle, seoDescription, seoKeywords, ogImageUrl, translations } = data;
if (
typeof slug === "string" &&
slug !== existing.slug &&
RESERVED_SLUGS.includes(slug.toLowerCase())
) {
set.status = 400;
return { success: false, error: "Reserved slug cannot be used" };
}
const updateData: any = {};
if (slug !== undefined) updateData.slug = slug;
if (title !== undefined) updateData.title = title;
if (isPublished !== undefined) updateData.isPublished = isPublished;
if (seoTitle !== undefined) updateData.seoTitle = seoTitle;
if (seoDescription !== undefined) updateData.seoDescription = seoDescription;
if (seoKeywords !== undefined) updateData.seoKeywords = seoKeywords;
if (ogImageUrl !== undefined) updateData.ogImageUrl = ogImageUrl;
let page = await prisma.page.update({
where: { id: existing.id },
data: updateData,
include: { translations: true },
});
if (translations) {
await savePageTranslations(page.id, translations);
page = await prisma.page.findUnique({
where: { id: page.id },
include: { translations: true, sections: { orderBy: { order: "asc" } } },
}) as any;
}
return { success: true, page };
} catch (err) {
console.error("[Pages] Update Error:", err);
set.status = 500;
return { error: "Failed to update page" };
}
},
{
body: t.Object({
slug: t.Optional(t.String()),
title: t.Optional(t.String()),
isPublished: t.Optional(t.Boolean()),
seoTitle: t.Optional(t.Nullable(t.String())),
seoDescription: t.Optional(t.Nullable(t.String())),
seoKeywords: t.Optional(t.Nullable(t.String())),
ogImageUrl: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Any()),
}),
},
)
@@ -818,41 +1044,12 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
})
.patch("/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id)) {
if (isNaN(id) || id <= 0) {
set.status = 400;
return { error: "Invalid page ID" };
}
const payload = body as any;
const results: any[] = [];
const LANGS = ["th", "en", "ja", "zh", "vi", "ko", "lo", "it"];
for (const lang of LANGS) {
const title =
typeof payload.title === "string"
? lang === "th"
? payload.title
: null
: payload.title?.[lang];
if (!title) continue;
const existing = await prisma.pageTranslation.findUnique({
where: { pageId_langCode: { pageId: id, langCode: lang } },
});
if (existing) {
const updated = await prisma.pageTranslation.update({
where: { id: existing.id },
data: { title },
});
results.push(updated);
} else {
const created = await prisma.pageTranslation.create({
data: { pageId: id, langCode: lang, title },
});
results.push(created);
}
}
const results = await savePageTranslations(id, body);
const updated = await prisma.page.findUnique({
where: { id },
@@ -860,7 +1057,23 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
});
return { success: true, translations: results, page: updated };
})
}, { body: t.Any() })
.post("/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id) || id <= 0) {
set.status = 400;
return { error: "Invalid page ID" };
}
const results = await savePageTranslations(id, body);
const updated = await prisma.page.findUnique({
where: { id },
include: { translations: true },
});
return { success: true, translations: results, page: updated };
}, { body: t.Any() })
.post(
"/:slug/sections",
async ({ params: { slug }, body, set }) => {
+287 -61
View File
@@ -2,6 +2,141 @@ import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
export async function saveStoreSettingsTranslations(settingsId: number, payload: any) {
const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
if (Array.isArray(payload)) {
for (const item of payload) {
const lang = item.langCode || item.lang;
if (!lang || !LANGS.includes(lang)) continue;
const updateData: any = {};
if (item.footerDescription !== undefined) updateData.footerDescription = item.footerDescription;
if (item.newsletterTitle !== undefined) updateData.newsletterTitle = item.newsletterTitle;
if (item.newsletterDescription !== undefined) updateData.newsletterDescription = item.newsletterDescription;
if (item.newsletterPlaceholder !== undefined) updateData.newsletterPlaceholder = item.newsletterPlaceholder;
if (item.newsletterButtonText !== undefined) updateData.newsletterButtonText = item.newsletterButtonText;
if (item.defaultSeoTitle !== undefined) updateData.defaultSeoTitle = item.defaultSeoTitle;
if (item.defaultSeoDescription !== undefined) updateData.defaultSeoDescription = item.defaultSeoDescription;
const existing = await prisma.storeSettingsTranslation.findUnique({
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
});
if (existing) {
const updated = await prisma.storeSettingsTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
} else {
const created = await prisma.storeSettingsTranslation.create({
data: { storeSettingsId: settingsId, langCode: lang, ...updateData }
});
results.push(created);
}
}
return results;
}
for (const lang of LANGS) {
const langObj = payload[lang];
const footerDescription = typeof langObj?.footerDescription === 'string' || langObj?.footerDescription === null
? langObj.footerDescription
: typeof payload.footerDescription === 'string' || payload.footerDescription === null
? (lang === 'th' ? payload.footerDescription : undefined)
: payload.footerDescription?.[lang];
const newsletterTitle = typeof langObj?.newsletterTitle === 'string' || langObj?.newsletterTitle === null
? langObj.newsletterTitle
: typeof payload.newsletterTitle === 'string' || payload.newsletterTitle === null
? (lang === 'th' ? payload.newsletterTitle : undefined)
: payload.newsletterTitle?.[lang];
const newsletterDescription = typeof langObj?.newsletterDescription === 'string' || langObj?.newsletterDescription === null
? langObj.newsletterDescription
: typeof payload.newsletterDescription === 'string' || payload.newsletterDescription === null
? (lang === 'th' ? payload.newsletterDescription : undefined)
: payload.newsletterDescription?.[lang];
const newsletterPlaceholder = typeof langObj?.newsletterPlaceholder === 'string' || langObj?.newsletterPlaceholder === null
? langObj.newsletterPlaceholder
: typeof payload.newsletterPlaceholder === 'string' || payload.newsletterPlaceholder === null
? (lang === 'th' ? payload.newsletterPlaceholder : undefined)
: payload.newsletterPlaceholder?.[lang];
const newsletterButtonText = typeof langObj?.newsletterButtonText === 'string' || langObj?.newsletterButtonText === null
? langObj.newsletterButtonText
: typeof payload.newsletterButtonText === 'string' || payload.newsletterButtonText === null
? (lang === 'th' ? payload.newsletterButtonText : undefined)
: payload.newsletterButtonText?.[lang];
const defaultSeoTitle = typeof langObj?.defaultSeoTitle === 'string' || langObj?.defaultSeoTitle === null
? langObj.defaultSeoTitle
: typeof payload.defaultSeoTitle === 'string' || payload.defaultSeoTitle === null
? (lang === 'th' ? payload.defaultSeoTitle : undefined)
: payload.defaultSeoTitle?.[lang];
const defaultSeoDescription = typeof langObj?.defaultSeoDescription === 'string' || langObj?.defaultSeoDescription === null
? langObj.defaultSeoDescription
: typeof payload.defaultSeoDescription === 'string' || payload.defaultSeoDescription === null
? (lang === 'th' ? payload.defaultSeoDescription : undefined)
: payload.defaultSeoDescription?.[lang];
if (
footerDescription === undefined &&
newsletterTitle === undefined &&
newsletterDescription === undefined &&
newsletterPlaceholder === undefined &&
newsletterButtonText === undefined &&
defaultSeoTitle === undefined &&
defaultSeoDescription === undefined
) {
continue;
}
const updateData: any = {};
if (footerDescription !== undefined) updateData.footerDescription = footerDescription;
if (newsletterTitle !== undefined) updateData.newsletterTitle = newsletterTitle;
if (newsletterDescription !== undefined) updateData.newsletterDescription = newsletterDescription;
if (newsletterPlaceholder !== undefined) updateData.newsletterPlaceholder = newsletterPlaceholder;
if (newsletterButtonText !== undefined) updateData.newsletterButtonText = newsletterButtonText;
if (defaultSeoTitle !== undefined) updateData.defaultSeoTitle = defaultSeoTitle;
if (defaultSeoDescription !== undefined) updateData.defaultSeoDescription = defaultSeoDescription;
const existing = await prisma.storeSettingsTranslation.findUnique({
where: { storeSettingsId_langCode: { storeSettingsId: settingsId, langCode: lang } }
});
if (existing) {
const updated = await prisma.storeSettingsTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
} else {
const created = await prisma.storeSettingsTranslation.create({
data: { storeSettingsId: settingsId, langCode: lang, ...updateData }
});
results.push(created);
}
if (lang === 'th') {
const baseUpdates: any = {};
if (footerDescription !== undefined) baseUpdates.footerDescription = footerDescription;
if (defaultSeoTitle !== undefined) baseUpdates.defaultSeoTitle = defaultSeoTitle;
if (defaultSeoDescription !== undefined) baseUpdates.defaultSeoDescription = defaultSeoDescription;
if (Object.keys(baseUpdates).length > 0) {
await prisma.storeSettings.update({
where: { id: settingsId },
data: baseUpdates
});
}
}
}
return results;
}
export const settingsRoute = new Elysia({ prefix: "/settings" })
.get("/", async ({ set, query }) => {
try {
@@ -13,19 +148,27 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
});
}
const { translations, ...settingsData } = settings;
const requestedLang = typeof query?.lang === "string" ? query.lang : undefined;
const localizedDescription = requestedLang
? translations.find((translation) => translation.langCode === requestedLang)?.footerDescription
const requestedLang = typeof (query as any)?.lang === "string" ? (query as any).lang : undefined;
const translation = requestedLang
? translations.find((t) => t.langCode === requestedLang)
: undefined;
const localizedDescription = translation?.footerDescription;
const localizedSeoTitle = translation?.defaultSeoTitle;
const localizedSeoDescription = translation?.defaultSeoDescription;
const headerBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior;
const mobileHeaderStyle = (settings.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
return {
...settingsData,
translations,
headerBehavior,
mobileHeaderBehavior,
mobileHeaderStyle,
footerDescription: localizedDescription ?? settingsData.footerDescription,
defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle,
defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription,
defaultOgImage: settingsData.defaultOgImage,
};
} catch (err) {
console.error("[Settings] Error:", err);
@@ -67,7 +210,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
}
})
.use(adminOnly)
.patch("/footer-brand", async ({ body, set }) => {
.patch("/footer-brand", async ({ body, set }: { body: any; set: any }) => {
try {
const payload = body as any;
const settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
@@ -118,9 +261,9 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
return { error: err?.message || "Failed to update footer brand" };
}
}, { body: t.Any() })
.put("/", async ({ body, set }) => {
.put("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, ...data } = body as any;
const { headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
@@ -136,18 +279,27 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
}
data.themeTokens = updatedTokens;
let settings = await prisma.storeSettings.findFirst();
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
if (!settings) {
settings = await prisma.storeSettings.create({ data });
settings = await prisma.storeSettings.create({ data, include: { translations: true } });
} else {
settings = await prisma.storeSettings.update({
where: { id: settings.id },
data
data,
include: { translations: true },
});
}
const returnedBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
const returnedMobileBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? returnedBehavior;
const returnedMobileStyle = (settings.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
if (translations) {
await saveStoreSettingsTranslations(settings.id, translations);
settings = (await prisma.storeSettings.findFirst({
include: { translations: true },
})) as any;
}
const returnedBehavior = (settings?.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
const returnedMobileBehavior = (settings?.themeTokens as any)?.mobileHeaderBehavior ?? returnedBehavior;
const returnedMobileStyle = (settings?.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
return {
...settings,
headerBehavior: returnedBehavior,
@@ -167,17 +319,17 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
bankAccountName: t.Optional(t.String()),
bankAccountNumber: t.Optional(t.String()),
pointsEarnRate: t.Optional(t.Number()),
logoUrl: t.Optional(t.String()),
logoUrl: t.Optional(t.Nullable(t.String())),
themePreset: t.Optional(t.String()),
themeTokens: t.Optional(t.Any()),
facebookUrl: t.Optional(t.String()),
instagramUrl: t.Optional(t.String()),
tiktokUrl: t.Optional(t.String()),
footerDescription: t.Optional(t.String()),
newsletterTitle: t.Optional(t.String()),
newsletterDescription: t.Optional(t.String()),
newsletterPlaceholder: t.Optional(t.String()),
newsletterButtonText: t.Optional(t.String()),
facebookUrl: t.Optional(t.Nullable(t.String())),
instagramUrl: t.Optional(t.Nullable(t.String())),
tiktokUrl: t.Optional(t.Nullable(t.String())),
footerDescription: t.Optional(t.Nullable(t.String())),
newsletterTitle: t.Optional(t.Nullable(t.String())),
newsletterDescription: t.Optional(t.Nullable(t.String())),
newsletterPlaceholder: t.Optional(t.Nullable(t.String())),
newsletterButtonText: t.Optional(t.Nullable(t.String())),
footerSupportHeading: t.Optional(t.Any()),
footerCopyrightText: t.Optional(t.Any()),
scrollRevealEnabled: t.Optional(t.Boolean()),
@@ -185,52 +337,106 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
scrollRevealDuration: t.Optional(t.Number()),
headerStyle: t.Optional(t.String()),
headerBehavior: t.Optional(t.String()),
mobileHeaderBehavior: t.Optional(t.String()),
mobileHeaderStyle: t.Optional(t.String()),
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
defaultOgImage: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Any()),
})
})
.patch("/translations", async ({ body, set }) => {
.post("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any;
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
const updatedTokens = { ...currentTokens };
if (headerBehavior !== undefined) {
updatedTokens.headerBehavior = headerBehavior;
}
if (mobileHeaderBehavior !== undefined) {
updatedTokens.mobileHeaderBehavior = mobileHeaderBehavior;
}
if (mobileHeaderStyle !== undefined) {
updatedTokens.mobileHeaderStyle = mobileHeaderStyle;
}
data.themeTokens = updatedTokens;
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
if (!settings) {
settings = await prisma.storeSettings.create({ data, include: { translations: true } });
} else {
settings = await prisma.storeSettings.update({
where: { id: settings.id },
data,
include: { translations: true },
});
}
if (translations) {
await saveStoreSettingsTranslations(settings.id, translations);
settings = (await prisma.storeSettings.findFirst({
include: { translations: true },
})) as any;
}
const returnedBehavior = (settings?.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll';
const returnedMobileBehavior = (settings?.themeTokens as any)?.mobileHeaderBehavior ?? returnedBehavior;
const returnedMobileStyle = (settings?.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
return {
...settings,
headerBehavior: returnedBehavior,
mobileHeaderBehavior: returnedMobileBehavior,
mobileHeaderStyle: returnedMobileStyle,
};
} catch (err) {
console.error("[Settings] Error:", err);
set.status = 500;
return { error: "Failed to update settings" };
}
}, {
body: t.Object({
brandName: t.Optional(t.String()),
shippingFee: t.Optional(t.Number()),
bankName: t.Optional(t.String()),
bankAccountName: t.Optional(t.String()),
bankAccountNumber: t.Optional(t.String()),
pointsEarnRate: t.Optional(t.Number()),
logoUrl: t.Optional(t.Nullable(t.String())),
themePreset: t.Optional(t.String()),
themeTokens: t.Optional(t.Any()),
facebookUrl: t.Optional(t.Nullable(t.String())),
instagramUrl: t.Optional(t.Nullable(t.String())),
tiktokUrl: t.Optional(t.Nullable(t.String())),
footerDescription: t.Optional(t.Nullable(t.String())),
newsletterTitle: t.Optional(t.Nullable(t.String())),
newsletterDescription: t.Optional(t.Nullable(t.String())),
newsletterPlaceholder: t.Optional(t.Nullable(t.String())),
newsletterButtonText: t.Optional(t.Nullable(t.String())),
footerSupportHeading: t.Optional(t.Any()),
footerCopyrightText: t.Optional(t.Any()),
scrollRevealEnabled: t.Optional(t.Boolean()),
scrollRevealStyle: t.Optional(t.String()),
scrollRevealDuration: t.Optional(t.Number()),
headerStyle: t.Optional(t.String()),
headerBehavior: t.Optional(t.String()),
mobileHeaderBehavior: t.Optional(t.String()),
mobileHeaderStyle: t.Optional(t.String()),
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
defaultOgImage: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Any()),
})
})
.patch("/translations", async ({ body, set }: { body: any; set: any }) => {
try {
let settings = await prisma.storeSettings.findFirst();
if (!settings) {
settings = await prisma.storeSettings.create({ data: {} });
}
const payload = body as any;
const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of LANGS) {
const footerDescription = typeof payload.footerDescription === 'string' ? (lang === 'th' ? payload.footerDescription : null) : payload.footerDescription?.[lang];
const newsletterTitle = typeof payload.newsletterTitle === 'string' ? (lang === 'th' ? payload.newsletterTitle : null) : payload.newsletterTitle?.[lang];
const newsletterDescription = typeof payload.newsletterDescription === 'string' ? (lang === 'th' ? payload.newsletterDescription : null) : payload.newsletterDescription?.[lang];
const newsletterPlaceholder = typeof payload.newsletterPlaceholder === 'string' ? (lang === 'th' ? payload.newsletterPlaceholder : null) : payload.newsletterPlaceholder?.[lang];
const newsletterButtonText = typeof payload.newsletterButtonText === 'string' ? (lang === 'th' ? payload.newsletterButtonText : null) : payload.newsletterButtonText?.[lang];
if (!footerDescription && !newsletterTitle && !newsletterDescription && !newsletterPlaceholder && !newsletterButtonText) continue;
const existing = await prisma.storeSettingsTranslation.findUnique({
where: { storeSettingsId_langCode: { storeSettingsId: settings.id, langCode: lang } }
});
const updateData: any = {};
if (footerDescription) updateData.footerDescription = footerDescription;
if (newsletterTitle) updateData.newsletterTitle = newsletterTitle;
if (newsletterDescription) updateData.newsletterDescription = newsletterDescription;
if (newsletterPlaceholder) updateData.newsletterPlaceholder = newsletterPlaceholder;
if (newsletterButtonText) updateData.newsletterButtonText = newsletterButtonText;
if (existing) {
const updated = await prisma.storeSettingsTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
} else {
const created = await prisma.storeSettingsTranslation.create({
data: { storeSettingsId: settings.id, langCode: lang, ...updateData }
});
results.push(created);
}
}
const results = await saveStoreSettingsTranslations(settings.id, body);
const updatedSettings = await prisma.storeSettings.findFirst({
include: { translations: true }
@@ -242,8 +448,28 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
set.status = 500;
return { error: "Failed to update settings translation" };
}
})
.post("/seed-pages", async ({ set }) => {
}, { body: t.Any() })
.post("/translations", async ({ body, set }: { body: any; set: any }) => {
try {
let settings = await prisma.storeSettings.findFirst();
if (!settings) {
settings = await prisma.storeSettings.create({ data: {} });
}
const results = await saveStoreSettingsTranslations(settings.id, body);
const updatedSettings = await prisma.storeSettings.findFirst({
include: { translations: true }
});
return { success: true, translations: results, settings: updatedSettings };
} catch (err) {
console.error("[Settings] Translation Error:", err);
set.status = 500;
return { error: "Failed to update settings translation" };
}
}, { body: t.Any() })
.post("/seed-pages", async ({ set }: { set: any }) => {
type SectionInput = { type: string; order: number; isActive: boolean; payload: object };
type PageDef = { slug: string; title: string; sections: SectionInput[] };