This commit is contained in:
Your Name committed 2026-10-10 08:00:56 +07:00
1 parent 943fd8f1a3
commit 9237c0eded
7 files changed
+417 -18

No files matched your search

@@ -14,7 +14,7 @@ import {
} from 'lucide-react'
interface ThemeLivePreviewProps {
tokens: Record<string, string>
tokens: Record<string, any>
}
// ── WCAG Relative Luminance & Contrast Calculation ──────────────────────────
@@ -401,6 +401,7 @@ export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
</div>
<div className="flex items-center gap-2">
{tokens.headerShowSearch !== false && (
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
@@ -409,7 +410,9 @@ export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
>
<Search className="w-3.5 h-3.5" />
</button>
)}
{tokens.headerShowCart !== false && (
<div className="relative">
<button
type="button"
@@ -426,7 +429,9 @@ export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
2
</span>
</div>
)}
{tokens.headerShowAccount !== false && (
<button
type="button"
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-1 cursor-pointer"
@@ -434,6 +439,7 @@ export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
<MessageCircle className="w-3 h-3" />
<span>LINE</span>
</button>
)}
</div>
</header>
+206 -2
View File
@@ -19,7 +19,13 @@ import {
ArrowUpDown,
Monitor,
Smartphone,
LayoutGrid
LayoutGrid,
Search,
Heart,
ShoppingBag,
Truck,
Globe,
User
} from 'lucide-react'
const defaultThemeTokens = {
@@ -169,6 +175,12 @@ export default function ThemeSettingsPage() {
const [mobileHeaderStyle, setMobileHeaderStyle] = useState<'logo_center' | 'logo_left' | 'search_bar_below'>('logo_center')
const [mobileHeaderBehavior, setMobileHeaderBehavior] = useState<'always_sticky' | 'hide_on_scroll'>('hide_on_scroll')
const [cardPreset, setCardPreset] = useState<'standard' | 'minimal' | 'swatch'>('standard')
const [headerShowSearch, setHeaderShowSearch] = useState<boolean>(true)
const [headerShowWishlist, setHeaderShowWishlist] = useState<boolean>(true)
const [headerShowCart, setHeaderShowCart] = useState<boolean>(true)
const [headerShowTrackOrder, setHeaderShowTrackOrder] = useState<boolean>(true)
const [headerShowLanguage, setHeaderShowLanguage] = useState<boolean>(true)
const [headerShowAccount, setHeaderShowAccount] = useState<boolean>(true)
// JSON Quick Copy / Paste State
const [isJsonBoxOpen, setIsJsonBoxOpen] = useState(false)
@@ -201,6 +213,12 @@ export default function ThemeSettingsPage() {
setMobileHeaderBehavior((data as any).mobileHeaderBehavior || (data.themeTokens as any)?.mobileHeaderBehavior || pcBehavior)
const cp = (data as any).cardPreset || (data.themeTokens as any)?.cardPreset || 'standard'
setCardPreset(cp === 'minimal_swatch' ? 'swatch' : cp)
setHeaderShowSearch((data as any).headerShowSearch ?? (data.themeTokens as any)?.headerShowSearch ?? true)
setHeaderShowWishlist((data as any).headerShowWishlist ?? (data.themeTokens as any)?.headerShowWishlist ?? true)
setHeaderShowCart((data as any).headerShowCart ?? (data.themeTokens as any)?.headerShowCart ?? true)
setHeaderShowTrackOrder((data as any).headerShowTrackOrder ?? (data.themeTokens as any)?.headerShowTrackOrder ?? true)
setHeaderShowLanguage((data as any).headerShowLanguage ?? (data.themeTokens as any)?.headerShowLanguage ?? true)
setHeaderShowAccount((data as any).headerShowAccount ?? (data.themeTokens as any)?.headerShowAccount ?? true)
})
.catch(console.error)
.finally(() => setLoading(false))
@@ -221,6 +239,12 @@ export default function ThemeSettingsPage() {
headerBehavior,
mobileHeaderBehavior,
mobileHeaderStyle,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
cardPreset,
},
cardPreset,
@@ -231,6 +255,12 @@ export default function ThemeSettingsPage() {
headerBehavior,
mobileHeaderStyle,
mobileHeaderBehavior,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
}
}
@@ -325,6 +355,25 @@ export default function ThemeSettingsPage() {
} else if (extractedTokens?.mobileHeaderBehavior === 'always_sticky' || extractedTokens?.mobileHeaderBehavior === 'hide_on_scroll') {
setMobileHeaderBehavior(extractedTokens.mobileHeaderBehavior)
}
// Extract Header Action Icons Visibility
if (typeof parsed.headerShowSearch === 'boolean') setHeaderShowSearch(parsed.headerShowSearch)
else if (typeof extractedTokens?.headerShowSearch === 'boolean') setHeaderShowSearch(extractedTokens.headerShowSearch)
if (typeof parsed.headerShowWishlist === 'boolean') setHeaderShowWishlist(parsed.headerShowWishlist)
else if (typeof extractedTokens?.headerShowWishlist === 'boolean') setHeaderShowWishlist(extractedTokens.headerShowWishlist)
if (typeof parsed.headerShowCart === 'boolean') setHeaderShowCart(parsed.headerShowCart)
else if (typeof extractedTokens?.headerShowCart === 'boolean') setHeaderShowCart(extractedTokens.headerShowCart)
if (typeof parsed.headerShowTrackOrder === 'boolean') setHeaderShowTrackOrder(parsed.headerShowTrackOrder)
else if (typeof extractedTokens?.headerShowTrackOrder === 'boolean') setHeaderShowTrackOrder(extractedTokens.headerShowTrackOrder)
if (typeof parsed.headerShowLanguage === 'boolean') setHeaderShowLanguage(parsed.headerShowLanguage)
else if (typeof extractedTokens?.headerShowLanguage === 'boolean') setHeaderShowLanguage(extractedTokens.headerShowLanguage)
if (typeof parsed.headerShowAccount === 'boolean') setHeaderShowAccount(parsed.headerShowAccount)
else if (typeof extractedTokens?.headerShowAccount === 'boolean') setHeaderShowAccount(extractedTokens.headerShowAccount)
}
// ── Paste JSON Text Handlers ──────────────────────────────────────────────
@@ -450,6 +499,12 @@ export default function ThemeSettingsPage() {
headerBehavior,
mobileHeaderBehavior,
mobileHeaderStyle,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
cardPreset,
},
cardPreset,
@@ -460,6 +515,12 @@ export default function ThemeSettingsPage() {
headerBehavior,
mobileHeaderStyle,
mobileHeaderBehavior,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
}),
})
if (res.ok) {
@@ -1262,6 +1323,141 @@ export default function ThemeSettingsPage() {
</button>
</div>
</div>
{/* Header Action Icons (Corner Icons) */}
<div className="space-y-4 pt-6 border-t border-white/5">
<div>
<label className="text-xs uppercase font-bold tracking-widest text-slate-400 flex items-center gap-1.5">
<ShoppingBag className="w-3.5 h-3.5 text-amber-400" />
<span>Header Action Icons (ไอคอนมุมขวาบนของ Header)</span>
</label>
<p className="text-xs text-slate-400 mt-1">
กำหนดเปิด/ซ่อนไอคอนการทำงานที่มุมขวาบนของ Header (ทั้งบน PC และ Mobile) เหมาะสำหรับร้านที่ไม่มีระบบตะกร้าสินค้า หรือต้องการซ่อนบางฟังก์ชัน
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{/* Cart */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowCart ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowCart ? 'bg-amber-400/20 text-amber-300' : 'bg-white/5 text-slate-500'}`}>
<ShoppingBag className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">ตะกร้าสินค้า (Cart)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">ซ่อนปุ่มตะกร้า หากเป็นร้านโชว์สินค้า/ไม่มีระบบสั่งซื้อ</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowCart(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowCart ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowCart ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
{/* Wishlist */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowWishlist ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowWishlist ? 'bg-rose-500/20 text-rose-300' : 'bg-white/5 text-slate-500'}`}>
<Heart className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">รายการโปรด (Wishlist)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">ไอคอนหัวใจบันทึกรายการโปรด</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowWishlist(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowWishlist ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowWishlist ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
{/* Search */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowSearch ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowSearch ? 'bg-cyan-500/20 text-cyan-300' : 'bg-white/5 text-slate-500'}`}>
<Search className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">ค้นหา (Search)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">ปุ่มเปิดหน้าต่างค้นหาสินค้า</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowSearch(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowSearch ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowSearch ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
{/* Track Order */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowTrackOrder ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowTrackOrder ? 'bg-emerald-500/20 text-emerald-300' : 'bg-white/5 text-slate-500'}`}>
<Truck className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">ติดตามพัสดุ (Track Order)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">ปุ่มตรวจสอบสถานะจัดส่งสินค้า</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowTrackOrder(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowTrackOrder ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowTrackOrder ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
{/* Language Switcher */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowLanguage ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowLanguage ? 'bg-violet-500/20 text-violet-300' : 'bg-white/5 text-slate-500'}`}>
<Globe className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">เปลี่ยนภาษา (Language)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">เมนูเลือกภาษา (TH, EN, JA ฯลฯ)</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowLanguage(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowLanguage ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowLanguage ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
{/* Account / Login */}
<div className={`flex items-start justify-between p-4 rounded-xl border transition-all ${headerShowAccount ? 'bg-white/5 border-amber-500/40' : 'bg-night-950/40 border-white/5 opacity-70'}`}>
<div className="flex items-start gap-3 min-w-0">
<div className={`p-2 rounded-lg shrink-0 ${headerShowAccount ? 'bg-lime-500/20 text-lime-300' : 'bg-white/5 text-slate-500'}`}>
<User className="w-4 h-4" />
</div>
<div>
<span className="text-xs font-bold text-white block">บัญชี / ล็อกอิน (Account)</span>
<span className="text-[10px] text-slate-400 block mt-0.5 leading-tight">ปุ่ม LINE Login หรือโปรไฟล์ผู้ใช้</span>
</div>
</div>
<button
type="button"
onClick={() => setHeaderShowAccount(v => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors cursor-pointer ml-2 ${headerShowAccount ? 'bg-amber-500' : 'bg-slate-700'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${headerShowAccount ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
</div>
</div>
</div>
</div>
{/* ── 5. Mobile Header Settings ── */}
@@ -1487,7 +1683,15 @@ export default function ThemeSettingsPage() {
{/* ── Right Column: Live Interactive UI Preview (Sticky) ── */}
<div className="xl:col-span-5 xl:sticky xl:top-20 space-y-6">
<ThemeLivePreview tokens={themeTokens} />
<ThemeLivePreview tokens={{
...themeTokens,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
}} />
</div>
</div>
+114 -8
View File
@@ -189,6 +189,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior;
const mobileHeaderStyle = (settings.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center';
const cardPreset = (settings.themeTokens as any)?.cardPreset ?? 'standard';
const headerShowSearch = (settings.themeTokens as any)?.headerShowSearch ?? true;
const headerShowWishlist = (settings.themeTokens as any)?.headerShowWishlist ?? true;
const headerShowCart = (settings.themeTokens as any)?.headerShowCart ?? true;
const headerShowTrackOrder = (settings.themeTokens as any)?.headerShowTrackOrder ?? true;
const headerShowLanguage = (settings.themeTokens as any)?.headerShowLanguage ?? true;
const headerShowAccount = (settings.themeTokens as any)?.headerShowAccount ?? true;
return {
...settingsData,
footerLayout: normalizeFooterLayoutSettings((settingsData as any).footerLayout),
@@ -197,6 +203,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
mobileHeaderBehavior,
mobileHeaderStyle,
cardPreset,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
footerDescription: localizedDescription ?? settingsData.footerDescription,
defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle,
defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription,
@@ -371,15 +383,33 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
}, { beforeHandle: requireAdmin, body: t.Any() })
.put("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, cardPreset, translations, ...data } = body as any;
const {
footerLayout,
headerBehavior,
mobileHeaderBehavior,
mobileHeaderStyle,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
cardPreset,
translations,
...data
} = body as any;
if (footerLayout !== undefined) {
set.status = 400;
return { error: "Use PATCH /settings/footer-layout to update footer layout" };
}
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
const existingTokens = (typeof (settings as any)?.themeTokens === 'object' && (settings as any)?.themeTokens !== null)
? (settings as any).themeTokens
: {};
const passedTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
const updatedTokens = { ...currentTokens };
const updatedTokens = { ...existingTokens, ...passedTokens };
if (headerBehavior !== undefined) {
updatedTokens.headerBehavior = headerBehavior;
}
@@ -389,12 +419,29 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
if (mobileHeaderStyle !== undefined) {
updatedTokens.mobileHeaderStyle = mobileHeaderStyle;
}
if (headerShowSearch !== undefined) {
updatedTokens.headerShowSearch = headerShowSearch;
}
if (headerShowWishlist !== undefined) {
updatedTokens.headerShowWishlist = headerShowWishlist;
}
if (headerShowCart !== undefined) {
updatedTokens.headerShowCart = headerShowCart;
}
if (headerShowTrackOrder !== undefined) {
updatedTokens.headerShowTrackOrder = headerShowTrackOrder;
}
if (headerShowLanguage !== undefined) {
updatedTokens.headerShowLanguage = headerShowLanguage;
}
if (headerShowAccount !== undefined) {
updatedTokens.headerShowAccount = headerShowAccount;
}
if (cardPreset !== undefined) {
updatedTokens.cardPreset = cardPreset;
}
data.themeTokens = updatedTokens;
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
if (!settings) {
settings = await prisma.storeSettings.create({ data, include: { translations: true } });
} else {
@@ -422,6 +469,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
mobileHeaderBehavior: returnedMobileBehavior,
mobileHeaderStyle: returnedMobileStyle,
cardPreset: returnedCardPreset,
headerShowSearch: (settings?.themeTokens as any)?.headerShowSearch ?? true,
headerShowWishlist: (settings?.themeTokens as any)?.headerShowWishlist ?? true,
headerShowCart: (settings?.themeTokens as any)?.headerShowCart ?? true,
headerShowTrackOrder: (settings?.themeTokens as any)?.headerShowTrackOrder ?? true,
headerShowLanguage: (settings?.themeTokens as any)?.headerShowLanguage ?? true,
headerShowAccount: (settings?.themeTokens as any)?.headerShowAccount ?? true,
};
} catch (err) {
console.error("[Settings] Error:", err);
@@ -456,6 +509,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
headerBehavior: t.Optional(t.String()),
mobileHeaderBehavior: t.Optional(t.String()),
mobileHeaderStyle: t.Optional(t.String()),
headerShowSearch: t.Optional(t.Boolean()),
headerShowWishlist: t.Optional(t.Boolean()),
headerShowCart: t.Optional(t.Boolean()),
headerShowTrackOrder: t.Optional(t.Boolean()),
headerShowLanguage: t.Optional(t.Boolean()),
headerShowAccount: t.Optional(t.Boolean()),
cardPreset: t.Optional(t.String()),
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
@@ -467,15 +526,33 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
})
.post("/", async ({ body, set }: { body: any; set: any }) => {
try {
const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, cardPreset, translations, ...data } = body as any;
const {
footerLayout,
headerBehavior,
mobileHeaderBehavior,
mobileHeaderStyle,
headerShowSearch,
headerShowWishlist,
headerShowCart,
headerShowTrackOrder,
headerShowLanguage,
headerShowAccount,
cardPreset,
translations,
...data
} = body as any;
if (footerLayout !== undefined) {
set.status = 400;
return { error: "Use PATCH /settings/footer-layout to update footer layout" };
}
const currentTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
const existingTokens = (typeof (settings as any)?.themeTokens === 'object' && (settings as any)?.themeTokens !== null)
? (settings as any).themeTokens
: {};
const passedTokens = (typeof data.themeTokens === 'object' && data.themeTokens !== null)
? data.themeTokens
: {};
const updatedTokens = { ...currentTokens };
const updatedTokens = { ...existingTokens, ...passedTokens };
if (headerBehavior !== undefined) {
updatedTokens.headerBehavior = headerBehavior;
}
@@ -485,12 +562,29 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
if (mobileHeaderStyle !== undefined) {
updatedTokens.mobileHeaderStyle = mobileHeaderStyle;
}
if (headerShowSearch !== undefined) {
updatedTokens.headerShowSearch = headerShowSearch;
}
if (headerShowWishlist !== undefined) {
updatedTokens.headerShowWishlist = headerShowWishlist;
}
if (headerShowCart !== undefined) {
updatedTokens.headerShowCart = headerShowCart;
}
if (headerShowTrackOrder !== undefined) {
updatedTokens.headerShowTrackOrder = headerShowTrackOrder;
}
if (headerShowLanguage !== undefined) {
updatedTokens.headerShowLanguage = headerShowLanguage;
}
if (headerShowAccount !== undefined) {
updatedTokens.headerShowAccount = headerShowAccount;
}
if (cardPreset !== undefined) {
updatedTokens.cardPreset = cardPreset;
}
data.themeTokens = updatedTokens;
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
if (!settings) {
settings = await prisma.storeSettings.create({ data, include: { translations: true } });
} else {
@@ -518,6 +612,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
mobileHeaderBehavior: returnedMobileBehavior,
mobileHeaderStyle: returnedMobileStyle,
cardPreset: returnedCardPreset,
headerShowSearch: (settings?.themeTokens as any)?.headerShowSearch ?? true,
headerShowWishlist: (settings?.themeTokens as any)?.headerShowWishlist ?? true,
headerShowCart: (settings?.themeTokens as any)?.headerShowCart ?? true,
headerShowTrackOrder: (settings?.themeTokens as any)?.headerShowTrackOrder ?? true,
headerShowLanguage: (settings?.themeTokens as any)?.headerShowLanguage ?? true,
headerShowAccount: (settings?.themeTokens as any)?.headerShowAccount ?? true,
};
} catch (err) {
console.error("[Settings] Error:", err);
@@ -552,6 +652,12 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
headerBehavior: t.Optional(t.String()),
mobileHeaderBehavior: t.Optional(t.String()),
mobileHeaderStyle: t.Optional(t.String()),
headerShowSearch: t.Optional(t.Boolean()),
headerShowWishlist: t.Optional(t.Boolean()),
headerShowCart: t.Optional(t.Boolean()),
headerShowTrackOrder: t.Optional(t.Boolean()),
headerShowLanguage: t.Optional(t.Boolean()),
headerShowAccount: t.Optional(t.Boolean()),
cardPreset: t.Optional(t.String()),
defaultSeoTitle: t.Optional(t.Nullable(t.String())),
defaultSeoDescription: t.Optional(t.Nullable(t.String())),
+19 -3
View File
@@ -199,9 +199,16 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
const isTwoTier = settings?.headerStyle === 'two_tier'
const showSearch = (settings?.themeTokens as any)?.headerShowSearch ?? settings?.headerShowSearch ?? true
const showWishlist = (settings?.themeTokens as any)?.headerShowWishlist ?? settings?.headerShowWishlist ?? true
const showCart = (settings?.themeTokens as any)?.headerShowCart ?? settings?.headerShowCart ?? true
const showLanguage = (settings?.themeTokens as any)?.headerShowLanguage ?? settings?.headerShowLanguage ?? true
const showAccount = (settings?.themeTokens as any)?.headerShowAccount ?? settings?.headerShowAccount ?? true
const renderActionButtons = () => (
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
{/* Search */}
{showSearch && (
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
@@ -211,8 +218,10 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
)}
{/* Wishlist */}
{showWishlist && (
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
@@ -226,8 +235,10 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</span>
)}
</Link>
)}
{/* Cart */}
{showCart && (
<button
onClick={() => { toggleCart(true); setMobileOpen(false); }}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
@@ -240,9 +251,10 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</span>
)}
</button>
)}
{/* Language switcher (desktop) */}
{isClientReady && isDesktop && (
{showLanguage && isClientReady && isDesktop && (
<div className="relative group flex items-center">
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-pop-black border border-pop-black/20 hover:bg-pop-black/5 bg-pop-bg flex items-center gap-1.5 uppercase tracking-wider">
<Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
@@ -269,7 +281,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
)}
{/* Auth / Profile (desktop) */}
{isClientReady && isDesktop && (
{showAccount && isClientReady && isDesktop && (
customer ? (
<div className="relative group flex items-center ml-1">
<Link to="/profile">
@@ -314,7 +326,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
const renderMobileActionButtons = (includeSearch = true) => (
<div className="flex items-center gap-1.5 shrink-0">
{includeSearch && (
{includeSearch && showSearch && (
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
@@ -326,6 +338,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</button>
)}
{showWishlist && (
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
@@ -339,7 +352,9 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</span>
)}
</Link>
)}
{showCart && (
<button
onClick={() => { toggleCart(true); setMobileOpen(false); }}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
@@ -352,6 +367,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</span>
)}
</button>
)}
</div>
)
+48 -1
View File
@@ -265,9 +265,16 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
const isTwoTier = settings?.headerStyle === 'two_tier'
const showSearch = (settings?.themeTokens as any)?.headerShowSearch ?? (settings as any)?.headerShowSearch ?? true
const showWishlist = (settings?.themeTokens as any)?.headerShowWishlist ?? (settings as any)?.headerShowWishlist ?? true
const showCart = (settings?.themeTokens as any)?.headerShowCart ?? (settings as any)?.headerShowCart ?? true
const showTrackOrder = (settings?.themeTokens as any)?.headerShowTrackOrder ?? (settings as any)?.headerShowTrackOrder ?? true
const showLanguage = (settings?.themeTokens as any)?.headerShowLanguage ?? (settings as any)?.headerShowLanguage ?? true
const showAccount = (settings?.themeTokens as any)?.headerShowAccount ?? (settings as any)?.headerShowAccount ?? true
const renderMobileActionButtons = (includeSearch = true) => (
<div className="flex items-center gap-1.5 shrink-0">
{includeSearch && (
{includeSearch && showSearch && (
<button
onClick={() => {
setSearchOpen(true)
@@ -280,6 +287,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</button>
)}
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
@@ -293,7 +301,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</Link>
)}
{showCart && (
<button
onClick={() => {
toggleCart(true)
@@ -309,7 +319,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</button>
)}
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
@@ -319,6 +331,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
)}
</div>
)
@@ -451,6 +464,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
<div className="flex items-center justify-end gap-1.5 md:gap-2">
{showSearch && (
<button
onClick={() => {
setSearchOpen(true)
@@ -461,7 +475,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
>
<Search className="h-4 w-4 transition-colors" />
</button>
)}
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
@@ -475,7 +491,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</Link>
)}
{showCart && (
<button
onClick={() => {
toggleCart(true)
@@ -491,7 +509,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</button>
)}
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
@@ -501,7 +521,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
)}
{showLanguage && (
<div className="hidden md:flex group relative items-center">
<button className="ui-nav-chip">
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
@@ -527,7 +549,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
</div>
</div>
)}
{showAccount && (
<div className="hidden md:flex relative items-center">
{customer ? (
<Link href="/profile" className="ml-1">
@@ -555,6 +579,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</button>
)}
</div>
)}
</div>
</div>
@@ -577,9 +602,12 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
<div className={`flex items-center gap-1.5 transition-all duration-300 ${scrolled ? 'w-auto opacity-100 md:gap-2' : 'w-0 opacity-0 overflow-hidden'}`}>
{showSearch && (
<button onClick={() => setSearchOpen(true)} className={iconButtonClass} aria-label="Search">
<Search className="h-4 w-4" />
</button>
)}
{showWishlist && (
<Link href="/wishlist" className={`relative ${iconButtonClass}`} aria-label="Wishlist">
<Heart className="h-4 w-4" />
{wishlistCount > 0 && (
@@ -588,6 +616,8 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</Link>
)}
{showCart && (
<button onClick={() => toggleCart(true)} className={`relative ${iconButtonClass}`} aria-label="Cart">
<ShoppingBag className="h-4 w-4" />
{cartCount > 0 && (
@@ -596,6 +626,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</button>
)}
</div>
</div>
</div>
@@ -622,6 +653,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
{showSearch && (
<button
onClick={() => {
setSearchOpen(true)
@@ -632,7 +664,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
>
<Search className="h-4 w-4 transition-colors" />
</button>
)}
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
@@ -646,7 +680,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</Link>
)}
{showCart && (
<button
onClick={() => {
toggleCart(true)
@@ -662,7 +698,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</span>
)}
</button>
)}
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
@@ -672,9 +710,11 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
)}
{canRenderDesktopMenu && (
<>
{showLanguage && (
<div className="group relative flex items-center">
<button className="ui-nav-chip">
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
@@ -700,7 +740,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
</div>
</div>
)}
{showAccount && (
<div className="relative flex items-center">
{customer ? (
<Link href="/profile" className="ml-1">
@@ -728,6 +770,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</button>
)}
</div>
)}
</>
)}
</div>
@@ -745,6 +788,10 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
onOpenLang={() => setLangSheetOpen(true)}
onLogin={loginWithLine}
onLogout={logout}
showLanguage={showLanguage}
showAccount={showAccount}
showTrackOrder={showTrackOrder}
showCart={showCart}
/>
)}
@@ -20,6 +20,10 @@ interface MobileDrawerProps {
onOpenLang: () => void
onLogin: () => void
onLogout: () => void
showLanguage?: boolean
showAccount?: boolean
showTrackOrder?: boolean
showCart?: boolean
}
export function MobileDrawer({
@@ -31,6 +35,10 @@ export function MobileDrawer({
onOpenLang,
onLogin,
onLogout,
showLanguage = true,
showAccount = true,
showTrackOrder = true,
showCart = true,
}: MobileDrawerProps) {
const [stack, setStack] = useState<MenuNode[]>([{ type: 'root', data: items, title: 'Menu' }])
@@ -81,18 +89,20 @@ export function MobileDrawer({
)}
</div>
))}
{customerName && (
{showAccount && customerName && (
<div className="border-b border-text-main/10">
<Link href="/profile" onClick={onClose} className={linkClass}>
My Profile
</Link>
</div>
)}
{showTrackOrder && (
<div className="border-b border-text-main/10">
<Link href="/track" onClick={onClose} className={linkClass}>
{lang === 'th' ? 'ติดตามคำสั่งซื้อ' : 'Track Order'}
</Link>
</div>
)}
</div>
)
}
@@ -167,6 +177,7 @@ export function MobileDrawer({
<div className="z-10 flex shrink-0 items-center justify-between border-b border-text-main/10 bg-surface-card px-6 py-5">
<span className="text-sm font-bold uppercase tracking-widest text-text-main">Menu</span>
<div className="flex items-center gap-3">
{showLanguage && (
<button
onClick={onOpenLang}
className="flex shrink-0 items-center gap-1 rounded-md border border-brand-primary/30 bg-surface-page px-2.5 py-1 text-[10px] font-bold uppercase text-brand-primary"
@@ -174,6 +185,7 @@ export function MobileDrawer({
<Globe className="h-3 w-3" />
{lang.toUpperCase()}
</button>
)}
<button onClick={onClose} className="rounded-full p-1.5 text-text-main/65 transition hover:bg-surface-page hover:text-brand-primary">
<X className="h-5 w-5" />
</button>
@@ -215,6 +227,7 @@ export function MobileDrawer({
</div>
<div className="z-10 shrink-0 space-y-3 border-t border-text-main/10 bg-surface-card px-6 pb-8 pt-6">
{showCart && (
<Link
href="/shop"
onClick={onClose}
@@ -222,7 +235,8 @@ export function MobileDrawer({
>
Shop Now
</Link>
{customerName ? (
)}
{showAccount && (customerName ? (
<button
onClick={() => {
onLogout()
@@ -242,7 +256,7 @@ export function MobileDrawer({
>
Login with LINE
</button>
)}
))}
</div>
</div>
</div>
+6
View File
@@ -417,6 +417,12 @@ export interface StoreSettings {
headerBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null
mobileHeaderStyle?: 'logo_center' | 'logo_left' | 'search_bar_below' | string | null
mobileHeaderBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null
headerShowSearch?: boolean | null
headerShowWishlist?: boolean | null
headerShowCart?: boolean | null
headerShowTrackOrder?: boolean | null
headerShowLanguage?: boolean | null
headerShowAccount?: boolean | null
cardPreset?: 'standard' | 'minimal' | 'swatch' | string | null
defaultSeoTitle?: string | null
defaultSeoDescription?: string | null