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
@@ -1,279 +0,0 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
export const productsRoute = new Elysia({ prefix: "/products" })
|
||||
.get("/scents", async () => {
|
||||
return await prisma.scent.findMany({
|
||||
orderBy: { name: "asc" }
|
||||
});
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
|
||||
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
|
||||
const search = query.search?.trim() || undefined;
|
||||
const lang = query.lang || 'th';
|
||||
// Pagination params
|
||||
const page = query.page ? parseInt(query.page) : 1;
|
||||
const limit = query.limit ? parseInt(query.limit) : 12;
|
||||
const skip = (page - 1) * limit;
|
||||
const where = {
|
||||
...(categoryId && { categoryId }),
|
||||
...(scentId && { scentId }),
|
||||
...(search && {
|
||||
OR: [
|
||||
{ translations: { some: { name: { contains: search, mode: "insensitive" } } } },
|
||||
{ scent: { name: { contains: search, mode: "insensitive" } } },
|
||||
{ category: { name: { contains: search, mode: "insensitive" } } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
const [products, total] = await Promise.all([
|
||||
prisma.product.findMany({
|
||||
where,
|
||||
include: {
|
||||
category: { include: { translations: true } },
|
||||
scent: true,
|
||||
translations: true,
|
||||
variants: {
|
||||
include: {
|
||||
bomItems: { include: { rawMaterial: true } }
|
||||
},
|
||||
orderBy: { price: "asc" }
|
||||
},
|
||||
images: { orderBy: { order: "asc" } },
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.product.count({ where }),
|
||||
]);
|
||||
// Map translations
|
||||
const mappedProducts = products.map(p => {
|
||||
if (lang === 'all') {
|
||||
const nameObj = {};
|
||||
const descObj = {};
|
||||
const detailsObj = {};
|
||||
p.translations.forEach(t => {
|
||||
nameObj[t.langCode] = t.name;
|
||||
descObj[t.langCode] = t.description;
|
||||
detailsObj[t.langCode] = t.details;
|
||||
});
|
||||
return { ...p, name: nameObj, description: descObj, details: detailsObj, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
|
||||
}
|
||||
else {
|
||||
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
|
||||
const catTrans = p.category.translations?.find((t) => t.langCode === lang) || p.category.translations?.find((t) => t.langCode === 'th') || p.category.translations?.[0];
|
||||
return {
|
||||
...p,
|
||||
name: trans?.name || '',
|
||||
description: trans?.description,
|
||||
details: trans?.details,
|
||||
category: { ...p.category, name: catTrans?.name || p.category.name }
|
||||
};
|
||||
}
|
||||
});
|
||||
mappedProducts.sort((a, b) => {
|
||||
const aName = typeof a.name === 'string' ? a.name : (a.name?.th || '');
|
||||
const bName = typeof b.name === 'string' ? b.name : (b.name?.th || '');
|
||||
return aName.localeCompare(bName);
|
||||
});
|
||||
return {
|
||||
data: mappedProducts,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
lastPage: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
})
|
||||
.get("/:id", async ({ params, query, status }) => {
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id))
|
||||
return status(400, { message: "Invalid product ID" });
|
||||
const lang = query.lang || 'th';
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
category: { include: { translations: true } },
|
||||
scent: true,
|
||||
translations: true,
|
||||
variants: {
|
||||
include: {
|
||||
bomItems: { include: { rawMaterial: true } }
|
||||
},
|
||||
orderBy: { price: "asc" }
|
||||
},
|
||||
images: { orderBy: { order: "asc" } },
|
||||
},
|
||||
});
|
||||
if (!product)
|
||||
return status(404, { message: "Product not found" });
|
||||
if (lang === 'all') {
|
||||
const nameObj = {};
|
||||
const descObj = {};
|
||||
const detailsObj = {};
|
||||
product.translations.forEach(t => {
|
||||
nameObj[t.langCode] = t.name;
|
||||
descObj[t.langCode] = t.description;
|
||||
detailsObj[t.langCode] = t.details;
|
||||
});
|
||||
return { ...product, name: nameObj, description: descObj, details: detailsObj };
|
||||
}
|
||||
else {
|
||||
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
|
||||
const catTrans = product.category.translations?.find((t) => t.langCode === lang) || product.category.translations?.find((t) => t.langCode === 'th') || product.category.translations?.[0];
|
||||
return {
|
||||
...product,
|
||||
name: trans?.name || '',
|
||||
description: trans?.description,
|
||||
details: trans?.details,
|
||||
category: { ...product.category, name: catTrans?.name || product.category.name }
|
||||
};
|
||||
}
|
||||
})
|
||||
.put("/:id", async ({ params, body, status }) => {
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id))
|
||||
return status(400, { message: "Invalid product ID" });
|
||||
const { categoryId, description, details, variants, images } = body;
|
||||
try {
|
||||
const updatedProduct = await prisma.$transaction(async (tx) => {
|
||||
// 1. Update basic product info
|
||||
const prod = await tx.product.update({
|
||||
where: { id },
|
||||
data: { categoryId },
|
||||
});
|
||||
// 2. Sync Variants
|
||||
await tx.productVariant.deleteMany({
|
||||
where: { productId: id },
|
||||
});
|
||||
if (variants && variants.length > 0) {
|
||||
for (const v of variants) {
|
||||
await tx.productVariant.create({
|
||||
data: {
|
||||
productId: id,
|
||||
size: v.size,
|
||||
price: v.price,
|
||||
bomItems: v.bomItems && v.bomItems.length > 0 ? {
|
||||
create: v.bomItems.map((b) => ({
|
||||
rawMaterialId: b.rawMaterialId,
|
||||
quantity: b.quantity,
|
||||
}))
|
||||
} : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// 3. Sync Images
|
||||
await tx.productImage.deleteMany({
|
||||
where: { productId: id },
|
||||
});
|
||||
if (images && images.length > 0) {
|
||||
for (const [idx, img] of images.entries()) {
|
||||
await tx.productImage.create({
|
||||
data: {
|
||||
productId: id,
|
||||
url: img.url,
|
||||
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return prod;
|
||||
});
|
||||
return { success: true, product: updatedProduct };
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Update product error:", err);
|
||||
return status(500, { success: false, message: "Failed to update product" });
|
||||
}
|
||||
}, {
|
||||
body: t.Any(),
|
||||
detail: {
|
||||
summary: "Update Product",
|
||||
tags: ["products"],
|
||||
},
|
||||
})
|
||||
.patch("/:id", async ({ params, body, status }) => {
|
||||
const id = parseInt(params.id);
|
||||
if (isNaN(id))
|
||||
return status(400, { message: "Invalid product ID" });
|
||||
try {
|
||||
const payload = body;
|
||||
const prodData = {};
|
||||
if (payload.categoryId !== undefined)
|
||||
prodData.categoryId = payload.categoryId;
|
||||
if (payload.scentId !== undefined)
|
||||
prodData.scentId = payload.scentId;
|
||||
if (payload.price !== undefined)
|
||||
prodData.price = payload.price;
|
||||
const prod = await prisma.product.update({
|
||||
where: { id },
|
||||
data: prodData,
|
||||
});
|
||||
// Update translations
|
||||
// If name, description, details are strings, treat as 'th'
|
||||
// If they are objects, iterate over them
|
||||
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
|
||||
for (const lang of languages) {
|
||||
const updateData = {};
|
||||
let shouldUpdate = false;
|
||||
if (payload.name) {
|
||||
const val = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name[lang];
|
||||
if (val !== undefined && val !== null) {
|
||||
updateData.name = val;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
if (payload.description) {
|
||||
const val = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description[lang];
|
||||
if (val !== undefined && val !== null) {
|
||||
updateData.description = val;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
if (payload.details) {
|
||||
const val = typeof payload.details === 'string' ? (lang === 'th' ? payload.details : null) : payload.details[lang];
|
||||
if (val !== undefined && val !== null) {
|
||||
updateData.details = val;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
// First check if translation exists to create or update
|
||||
const existing = await prisma.productTranslation.findUnique({
|
||||
where: { productId_langCode: { productId: id, langCode: lang } }
|
||||
});
|
||||
if (existing) {
|
||||
await prisma.productTranslation.update({
|
||||
where: { id: existing.id },
|
||||
data: updateData
|
||||
});
|
||||
}
|
||||
else if (updateData.name) { // name is required for creation
|
||||
await prisma.productTranslation.create({
|
||||
data: {
|
||||
productId: id,
|
||||
langCode: lang,
|
||||
name: updateData.name,
|
||||
description: updateData.description,
|
||||
details: updateData.details
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return { success: true, product: prod };
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Patch product error:", err);
|
||||
return status(500, { success: false, message: "Failed to patch product" });
|
||||
}
|
||||
}, {
|
||||
body: t.Any(),
|
||||
detail: {
|
||||
summary: "Partial Update Product",
|
||||
tags: ["products"],
|
||||
},
|
||||
});
|
||||
//# sourceMappingURL=products.js.map
|
||||
Reference in new issue
Block a user