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

No files matched your search

+33 -27
View File
@@ -14,7 +14,7 @@ import {
} from 'lucide-react'
interface ThemeLivePreviewProps {
tokens: Record<string, string>
tokens: Record<string, any>
}
// ── WCAG Relative Luminance & Contrast Calculation ──────────────────────────
@@ -401,39 +401,45 @@ export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
</div>
<div className="flex items-center gap-2">
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
className="p-1 rounded-full hover:bg-black/5 transition"
title="Search"
>
<Search className="w-3.5 h-3.5" />
</button>
<div className="relative">
{tokens.headerShowSearch !== false && (
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
className="p-1 rounded-full hover:bg-black/5 transition"
title="Cart"
title="Search"
>
<ShoppingCart className="w-3.5 h-3.5" />
<Search className="w-3.5 h-3.5" />
</button>
<span
style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}
className="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full text-[8px] font-bold flex items-center justify-center shadow-sm"
>
2
</span>
</div>
)}
<button
type="button"
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-1 cursor-pointer"
>
<MessageCircle className="w-3 h-3" />
<span>LINE</span>
</button>
{tokens.headerShowCart !== false && (
<div className="relative">
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
className="p-1 rounded-full hover:bg-black/5 transition"
title="Cart"
>
<ShoppingCart className="w-3.5 h-3.5" />
</button>
<span
style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}
className="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full text-[8px] font-bold flex items-center justify-center shadow-sm"
>
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"
>
<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())),
+78 -62
View File
@@ -199,50 +199,62 @@ 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 */}
<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"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-pop-black transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
{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"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-pop-black transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 */}
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
{showWishlist && (
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
)}
{/* Cart */}
<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"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
{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"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</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,32 +338,36 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
</button>
)}
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
{showWishlist && (
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
)}
<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"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
{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"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
)}
</div>
)
+303 -256
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,45 +287,51 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</button>
)}
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
)}
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
{showCart && (
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
)}
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={iconButtonClass}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={iconButtonClass}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
)}
</div>
)
@@ -451,231 +464,67 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
<div className="flex items-center justify-end gap-1.5 md:gap-2">
<button
onClick={() => {
setSearchOpen(true)
setMobileOpen(false)
}}
className={iconButtonClass}
aria-label="Search"
>
<Search className="h-4 w-4 transition-colors" />
</button>
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
<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} />
{activeLanguage.short}
{showSearch && (
<button
onClick={() => {
setSearchOpen(true)
setMobileOpen(false)
}}
className={iconButtonClass}
aria-label="Search"
>
<Search className="h-4 w-4 transition-colors" />
</button>
<div className="pointer-events-none absolute right-0 top-full w-36 pt-2 opacity-0 transition-all duration-300 group-hover:pointer-events-auto group-hover:opacity-100">
<div className="ui-nav-panel space-y-0.5 p-1.5">
{LANGUAGES.map((language) => {
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
return (
<button
key={language.code}
onClick={() => changeLanguage(language.code)}
className={`flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-xs font-medium transition-colors ${
isActive ? 'bg-surface-page font-bold text-text-main' : 'text-text-main/65 hover:bg-surface-page hover:text-brand-primary'
}`}
>
<span>{language.label}</span>
{isActive && <Check className="h-3.5 w-3.5" />}
</button>
)
})}
</div>
</div>
</div>
<div className="hidden md:flex relative items-center">
{customer ? (
<Link href="/profile" className="ml-1">
{customer.pictureUrl ? (
<Image
src={customer.pictureUrl}
alt={customer.displayName || 'Profile'}
width={36}
height={36}
unoptimized
className="h-9 w-9 rounded-full border-2 border-brand-primary/30 object-cover"
/>
) : (
<span className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-brand-primary/30 text-text-main/65">
<User className="h-4 w-4" />
</span>
)}
</Link>
) : (
<button
onClick={loginWithLine}
className="ui-button-line ml-2"
>
{t('nav.login', 'เข้าสู่ระบบด้วย LINE')}
</button>
)}
</div>
</div>
</div>
{/* Bottom Tier: Centered Navigation (Desktop only) */}
<div className={`hidden md:flex w-full items-center justify-between transition-all duration-300 ${
scrolled ? 'py-0.5' : 'border-t border-black/5 pt-2 mt-1'
}`}>
<div className={`transition-all duration-300 ${scrolled ? 'w-auto opacity-100' : 'w-0 opacity-0 overflow-hidden'}`}>
<Link href="/" className="ui-brand-wordmark text-sm font-bold tracking-widest">
{settings?.logoUrl ? (
<Image src={settings.logoUrl} alt={brandName} width={120} height={28} unoptimized className="h-6 w-auto object-contain" />
) : (
brandName
)}
</Link>
</div>
<div className="flex flex-1 justify-center">
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
</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'}`}>
<button onClick={() => setSearchOpen(true)} className={iconButtonClass} aria-label="Search">
<Search className="h-4 w-4" />
</button>
<Link href="/wishlist" className={`relative ${iconButtonClass}`} aria-label="Wishlist">
<Heart className="h-4 w-4" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
<button onClick={() => toggleCart(true)} className={`relative ${iconButtonClass}`} aria-label="Cart">
<ShoppingBag className="h-4 w-4" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
</div>
</div>
</div>
) : (
<div className="hidden md:flex w-full items-center justify-between gap-4">
<div className="flex shrink-0 items-center">
<Link
href="/"
onClick={() => setMobileOpen(false)}
className="ui-brand-wordmark"
>
{settings?.logoUrl ? (
<Image src={settings.logoUrl} alt={brandName} width={160} height={40} unoptimized className="h-8 w-auto object-contain md:h-10" />
) : (
brandName
)}
</Link>
</div>
<div className="flex flex-1 justify-center">
{canRenderDesktopMenu && (
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
)}
</div>
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
<button
onClick={() => {
setSearchOpen(true)
setMobileOpen(false)
}}
className={iconButtonClass}
aria-label="Search"
>
<Search className="h-4 w-4 transition-colors" />
</button>
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
)}
</Link>
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
{showCart && (
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
)}
</button>
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<Truck className="h-4 w-4 transition-colors" />
</Link>
)}
{canRenderDesktopMenu && (
<>
<div className="group relative flex items-center">
{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} />
{activeLanguage.short}
@@ -700,8 +549,10 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</div>
</div>
</div>
)}
<div className="relative flex items-center">
{showAccount && (
<div className="hidden md:flex relative items-center">
{customer ? (
<Link href="/profile" className="ml-1">
{customer.pictureUrl ? (
@@ -728,6 +579,198 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
</button>
)}
</div>
)}
</div>
</div>
{/* Bottom Tier: Centered Navigation (Desktop only) */}
<div className={`hidden md:flex w-full items-center justify-between transition-all duration-300 ${
scrolled ? 'py-0.5' : 'border-t border-black/5 pt-2 mt-1'
}`}>
<div className={`transition-all duration-300 ${scrolled ? 'w-auto opacity-100' : 'w-0 opacity-0 overflow-hidden'}`}>
<Link href="/" className="ui-brand-wordmark text-sm font-bold tracking-widest">
{settings?.logoUrl ? (
<Image src={settings.logoUrl} alt={brandName} width={120} height={28} unoptimized className="h-6 w-auto object-contain" />
) : (
brandName
)}
</Link>
</div>
<div className="flex flex-1 justify-center">
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
</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 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
)}
{showCart && (
<button onClick={() => toggleCart(true)} className={`relative ${iconButtonClass}`} aria-label="Cart">
<ShoppingBag className="h-4 w-4" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
)}
</div>
</div>
</div>
) : (
<div className="hidden md:flex w-full items-center justify-between gap-4">
<div className="flex shrink-0 items-center">
<Link
href="/"
onClick={() => setMobileOpen(false)}
className="ui-brand-wordmark"
>
{settings?.logoUrl ? (
<Image src={settings.logoUrl} alt={brandName} width={160} height={40} unoptimized className="h-8 w-auto object-contain md:h-10" />
) : (
brandName
)}
</Link>
</div>
<div className="flex flex-1 justify-center">
{canRenderDesktopMenu && (
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
)}
</div>
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
{showSearch && (
<button
onClick={() => {
setSearchOpen(true)
setMobileOpen(false)
}}
className={iconButtonClass}
aria-label="Search"
>
<Search className="h-4 w-4 transition-colors" />
</button>
)}
{showWishlist && (
<Link
href="/wishlist"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label="Wishlist"
>
<Heart className="h-4 w-4 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{wishlistCount}
</span>
)}
</Link>
)}
{showCart && (
<button
onClick={() => {
toggleCart(true)
setMobileOpen(false)
}}
className={`relative ${iconButtonClass}`}
aria-label="Cart"
>
<ShoppingBag className="h-4 w-4 transition-colors" />
{cartCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-brand-primary text-[9px] font-bold text-surface-card">
{cartCount}
</span>
)}
</button>
)}
{showTrackOrder && (
<Link
href="/track"
onClick={() => setMobileOpen(false)}
className={`relative ${iconButtonClass}`}
aria-label={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
title={t('nav.trackOrder', 'ติดตามคำสั่งซื้อ')}
>
<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} />
{activeLanguage.short}
</button>
<div className="pointer-events-none absolute right-0 top-full w-36 pt-2 opacity-0 transition-all duration-300 group-hover:pointer-events-auto group-hover:opacity-100">
<div className="ui-nav-panel space-y-0.5 p-1.5">
{LANGUAGES.map((language) => {
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
return (
<button
key={language.code}
onClick={() => changeLanguage(language.code)}
className={`flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-xs font-medium transition-colors ${
isActive ? 'bg-surface-page font-bold text-text-main' : 'text-text-main/65 hover:bg-surface-page hover:text-brand-primary'
}`}
>
<span>{language.label}</span>
{isActive && <Check className="h-3.5 w-3.5" />}
</button>
)
})}
</div>
</div>
</div>
)}
{showAccount && (
<div className="relative flex items-center">
{customer ? (
<Link href="/profile" className="ml-1">
{customer.pictureUrl ? (
<Image
src={customer.pictureUrl}
alt={customer.displayName || 'Profile'}
width={36}
height={36}
unoptimized
className="h-9 w-9 rounded-full border-2 border-brand-primary/30 object-cover"
/>
) : (
<span className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-brand-primary/30 text-text-main/65">
<User className="h-4 w-4" />
</span>
)}
</Link>
) : (
<button
onClick={loginWithLine}
className="ui-button-line ml-2"
>
{t('nav.login', 'เข้าสู่ระบบด้วย LINE')}
</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>
)}
<div className="border-b border-text-main/10">
<Link href="/track" onClick={onClose} className={linkClass}>
{lang === 'th' ? 'ติดตามคำสั่งซื้อ' : 'Track Order'}
</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,13 +177,15 @@ 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">
<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"
>
<Globe className="h-3 w-3" />
{lang.toUpperCase()}
</button>
{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"
>
<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,14 +227,16 @@ 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">
<Link
href="/shop"
onClick={onClose}
className="block w-full rounded-xl bg-text-main py-3.5 text-center text-xs font-bold uppercase tracking-widest text-surface-card shadow-sm transition-colors hover:bg-brand-primary"
>
Shop Now
</Link>
{customerName ? (
{showCart && (
<Link
href="/shop"
onClick={onClose}
className="block w-full rounded-xl bg-text-main py-3.5 text-center text-xs font-bold uppercase tracking-widest text-surface-card shadow-sm transition-colors hover:bg-brand-primary"
>
Shop Now
</Link>
)}
{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