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

+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[] };