feat(cms): implement multi-language support across frontend and admin UI
This commit is contained in:
1 parent
730e2a7b48
commit
eac45dd4a7
62 files changed
+1377
-5116
No files matched your search
@@ -232,6 +232,46 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
return { error: "Failed to delete page" };
|
||||
}
|
||||
})
|
||||
.patch("/:id/translations", async ({ params, body, set }) => {
|
||||
const id = Number(params.id);
|
||||
if (isNaN(id)) {
|
||||
set.status = 400;
|
||||
return { error: "Invalid page ID" };
|
||||
}
|
||||
|
||||
const payload = body as any;
|
||||
const results: any[] = [];
|
||||
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
||||
|
||||
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 updated = await prisma.page.findUnique({
|
||||
where: { id },
|
||||
include: { translations: true }
|
||||
});
|
||||
|
||||
return { success: true, translations: results, page: updated };
|
||||
})
|
||||
.post("/:slug/sections", async ({ params: { slug }, body, set }) => {
|
||||
try {
|
||||
const pageId = Number(slug);
|
||||
@@ -348,6 +388,11 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
|
||||
}
|
||||
});
|
||||
|
||||
function isValidText(val: any, allowEmpty: boolean = false): boolean {
|
||||
if (typeof val === 'string') return allowEmpty ? true : val.trim().length > 0;
|
||||
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
||||
}
|
||||
|
||||
function validateSectionPayload(type: string, payload: any): string | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return "Payload must be a JSON object";
|
||||
@@ -355,23 +400,23 @@ function validateSectionPayload(type: string, payload: any): string | null {
|
||||
|
||||
switch (type.toUpperCase()) {
|
||||
case 'HERO':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Hero payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Hero payload must contain a string description";
|
||||
if (!isValidText(payload.title)) return "Hero payload must contain a non-empty title (string or object)";
|
||||
if (payload.description === undefined || !isValidText(payload.description, true)) return "Hero payload must contain a description (string or object)";
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Hero payload must contain a non-empty string imageUrl";
|
||||
if (typeof payload.tagline !== 'string') return "Hero payload must contain a string tagline";
|
||||
if (typeof payload.buttonText !== 'string') return "Hero payload must contain a string buttonText";
|
||||
if (payload.tagline === undefined || !isValidText(payload.tagline, true)) return "Hero payload must contain a tagline (string or object)";
|
||||
if (payload.buttonText === undefined || !isValidText(payload.buttonText, true)) return "Hero payload must contain a buttonText (string or object)";
|
||||
if (typeof payload.buttonLink !== 'string') return "Hero payload must contain a string buttonLink";
|
||||
break;
|
||||
case 'CATEGORIES':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Categories payload must contain a non-empty string title";
|
||||
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
|
||||
if (!isValidText(payload.title)) return "Categories payload must contain a non-empty title (string or object)";
|
||||
if (payload.subtitle !== undefined && !isValidText(payload.subtitle, true)) return "Subtitle must be a string or object";
|
||||
if (payload.categoryIds !== undefined && !Array.isArray(payload.categoryIds)) {
|
||||
return "categoryIds must be an array of numbers";
|
||||
}
|
||||
break;
|
||||
case 'PRODUCTS':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Products payload must contain a non-empty string title";
|
||||
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
|
||||
if (!isValidText(payload.title)) return "Products payload must contain a non-empty title (string or object)";
|
||||
if (payload.subtitle !== undefined && !isValidText(payload.subtitle, true)) return "Subtitle must be a string or object";
|
||||
if (!['featured', 'scent', 'custom'].includes(payload.type)) {
|
||||
return "Products type must be 'featured', 'scent', or 'custom'";
|
||||
}
|
||||
@@ -394,41 +439,41 @@ function validateSectionPayload(type: string, payload: any): string | null {
|
||||
case 'BANNER':
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Banner payload must contain a non-empty string imageUrl";
|
||||
if (payload.linkUrl !== undefined && typeof payload.linkUrl !== 'string') return "linkUrl must be a string";
|
||||
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
|
||||
if (payload.description !== undefined && typeof payload.description !== 'string') return "description must be a string";
|
||||
if (payload.buttonText !== undefined && typeof payload.buttonText !== 'string') return "buttonText must be a string";
|
||||
if (payload.title !== undefined && !isValidText(payload.title, true)) return "title must be a string or object";
|
||||
if (payload.description !== undefined && !isValidText(payload.description, true)) return "description must be a string or object";
|
||||
if (payload.buttonText !== undefined && !isValidText(payload.buttonText, true)) return "buttonText must be a string or object";
|
||||
break;
|
||||
case 'TEXT':
|
||||
if (typeof payload.content !== 'string' || !payload.content.trim()) return "Text payload must contain a non-empty string content";
|
||||
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
|
||||
if (!isValidText(payload.content)) return "Text payload must contain non-empty content (string or object)";
|
||||
if (payload.title !== undefined && !isValidText(payload.title, true)) return "title must be a string or object";
|
||||
if (payload.textAlign !== undefined && !['left', 'center', 'right'].includes(payload.textAlign)) {
|
||||
return "textAlign must be 'left', 'center', or 'right'";
|
||||
}
|
||||
break;
|
||||
case 'ANNOUNCEMENT_BAR':
|
||||
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Announcement Bar requires an array of slides";
|
||||
if (payload.slides.some((s: any) => typeof s !== 'string')) return "All slides must be strings";
|
||||
if (payload.slides.some((s: any) => !isValidText(s, true))) return "All slides must be strings or objects";
|
||||
break;
|
||||
case 'NEWSLETTER':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Newsletter payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Newsletter description must be a string";
|
||||
if (typeof payload.buttonText !== 'string' || !payload.buttonText.trim()) return "Newsletter buttonText must be a non-empty string";
|
||||
if (!isValidText(payload.title)) return "Newsletter payload must contain a non-empty title (string or object)";
|
||||
if (payload.description === undefined || !isValidText(payload.description, true)) return "Newsletter description must be a string or object";
|
||||
if (!isValidText(payload.buttonText)) return "Newsletter buttonText must be a non-empty string or object";
|
||||
if (typeof payload.showPrivacyCheckbox !== 'boolean') return "showPrivacyCheckbox must be a boolean";
|
||||
break;
|
||||
case 'EDITORIAL_TILE':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Tile payload must contain a non-empty string title";
|
||||
if (typeof payload.description !== 'string') return "Editorial Tile description must be a string";
|
||||
if (!isValidText(payload.title)) return "Editorial Tile payload must contain a non-empty title (string or object)";
|
||||
if (payload.description === undefined || !isValidText(payload.description, true)) return "Editorial Tile description must be a string or object";
|
||||
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Editorial Tile imageUrl must be a non-empty string";
|
||||
if (!['left', 'right'].includes(payload.imagePosition)) return "imagePosition must be 'left' or 'right'";
|
||||
break;
|
||||
case 'EDITORIAL_CAROUSEL':
|
||||
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Carousel payload must contain a non-empty string title";
|
||||
if (!isValidText(payload.title)) return "Editorial Carousel payload must contain a non-empty title (string or object)";
|
||||
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Editorial Carousel requires an array of slides";
|
||||
for (const [idx, slide] of payload.slides.entries()) {
|
||||
if (typeof slide.title !== 'string' || !slide.title.trim()) return `Slide #${idx+1} must contain a non-empty title`;
|
||||
if (typeof slide.description !== 'string') return `Slide #${idx+1} must contain a description string`;
|
||||
if (!isValidText(slide.title)) return `Slide #${idx+1} must contain a non-empty title (string or object)`;
|
||||
if (slide.description === undefined || !isValidText(slide.description, true)) return `Slide #${idx+1} must contain a description (string or object)`;
|
||||
if (typeof slide.imageUrl !== 'string' || !slide.imageUrl.trim()) return `Slide #${idx+1} must contain a non-empty imageUrl`;
|
||||
if (typeof slide.category !== 'string') return `Slide #${idx+1} must contain a category string`;
|
||||
if (slide.category === undefined || !isValidText(slide.category, true)) return `Slide #${idx+1} must contain a category (string or object)`;
|
||||
}
|
||||
break;
|
||||
case 'HERO_ARTICLE': {
|
||||
|
||||
Reference in new issue
Block a user