x
This commit is contained in:
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");
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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[] };
|
||||
|
||||
|
||||
Reference in new issue
Block a user