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 }) => {