This commit is contained in:
Your Name committed 2026-06-18 21:59:16 -04:00
1 parent feeda6dc60
commit 927f03a8b7
12 files changed
+95 -10

No files matched your search

@@ -7,6 +7,7 @@ export default function SystemSettingsPage() {
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
// Form State // Form State
const [brandName, setBrandName] = useState('')
const [shippingFee, setShippingFee] = useState<string>('') const [shippingFee, setShippingFee] = useState<string>('')
const [bankName, setBankName] = useState('') const [bankName, setBankName] = useState('')
const [bankAccountName, setBankAccountName] = useState('') const [bankAccountName, setBankAccountName] = useState('')
@@ -29,6 +30,7 @@ export default function SystemSettingsPage() {
fetch(`${API_BASE}/settings`) fetch(`${API_BASE}/settings`)
.then(r => r.json()) .then(r => r.json())
.then((data: StoreSettings) => { .then((data: StoreSettings) => {
setBrandName(data.brandName || 'HomAroma')
setShippingFee(data.shippingFee.toString()) setShippingFee(data.shippingFee.toString())
setBankName(data.bankName || '') setBankName(data.bankName || '')
setBankAccountName(data.bankAccountName || '') setBankAccountName(data.bankAccountName || '')
@@ -58,6 +60,7 @@ export default function SystemSettingsPage() {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
brandName,
shippingFee: parseFloat(shippingFee) || 0, shippingFee: parseFloat(shippingFee) || 0,
bankName, bankName,
bankAccountName, bankAccountName,
@@ -208,6 +211,19 @@ export default function SystemSettingsPage() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2">
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Brand Name</label>
<input
type="text"
required
value={brandName}
onChange={e => setBrandName(e.target.value)}
className="admin-input w-full max-w-sm"
placeholder="HomAroma"
/>
<p className="text-[10px] text-slate-500">This name is used by the website and spa header/footer as the brand display name.</p>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Footer Description</label> <label className="text-xs uppercase font-bold tracking-widest text-slate-500">Footer Description</label>
<textarea <textarea
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "StoreSettings" ADD COLUMN "brandName" TEXT NOT NULL DEFAULT 'HomAroma';
+1
View File
@@ -207,6 +207,7 @@ model Promotion {
model StoreSettings { model StoreSettings {
id Int @id @default(1) id Int @id @default(1)
brandName String @default("HomAroma")
shippingFee Float @default(35) shippingFee Float @default(35)
bankName String @default("KASIKORNBANK (KBANK)") bankName String @default("KASIKORNBANK (KBANK)")
bankAccountName String @default("HomAroma Co., Ltd.") bankAccountName String @default("HomAroma Co., Ltd.")
+1
View File
@@ -39,6 +39,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
} }
}, { }, {
body: t.Object({ body: t.Object({
brandName: t.String(),
shippingFee: t.Number(), shippingFee: t.Number(),
bankName: t.String(), bankName: t.String(),
bankAccountName: t.String(), bankAccountName: t.String(),
+6
View File
@@ -28,6 +28,12 @@ bun run dev
ถ้าไม่ตั้งค่า `MCP_BACKEND_URL` server จะเริ่มที่ local backend และสลับเป็น production ได้ด้วย tool `set_target_environment` ถ้าไม่ตั้งค่า `MCP_BACKEND_URL` server จะเริ่มที่ local backend และสลับเป็น production ได้ด้วย tool `set_target_environment`
## Available tools
- `update_brand_name` เปลี่ยนชื่อแบรนด์กลางที่ใช้ใน header/footer ของ `apps/spa` และ `apps/website`
- `update_store_settings_translation` อัปเดตข้อความแปลของ store settings
- `update_footer_content` อัปเดตเนื้อหา footer และลิงก์ที่เกี่ยวข้อง
## Scripts ## Scripts
- `bun run dev` - `bun run dev`
+42
View File
@@ -477,6 +477,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["lang"] required: ["lang"]
} }
}, },
{
name: "update_brand_name",
description: "Update the brand name used across website and SPA headers, footers, and brand displays.",
inputSchema: {
type: "object",
properties: {
brandName: { type: "string", description: "Brand display name to store in system settings" }
},
required: ["brandName"]
}
},
{ {
name: "update_article_translation", name: "update_article_translation",
description: "Add or update translations for a specific article.", description: "Add or update translations for a specific article.",
@@ -890,6 +901,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const res = await api.patch(`/settings/translations`, payload); const res = await api.patch(`/settings/translations`, payload);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
} }
case "update_brand_name": {
const { brandName } = params as any;
const currentSettingsRes = await api.get("/settings");
const currentSettings = currentSettingsRes.data || {};
const mergedSettings = { ...currentSettings, brandName };
for (const key in mergedSettings) {
if (mergedSettings[key] === null) {
delete mergedSettings[key];
}
}
const res = await api.put("/settings", mergedSettings);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
tool: "update_brand_name",
brandName: res.data?.brandName ?? brandName,
settings: res.data
},
null,
2
)
}
]
};
}
case "update_article_translation": { case "update_article_translation": {
const { id, lang, title, content } = params as any; const { id, lang, title, content } = params as any;
const payload: any = {}; const payload: any = {};
+3 -2
View File
@@ -11,6 +11,7 @@ export default function Footer({ categories = [] }: FooterProps) {
const [exploreMenu, setExploreMenu] = useState<MenuItem[]>([]) const [exploreMenu, setExploreMenu] = useState<MenuItem[]>([])
const [supportMenu, setSupportMenu] = useState<MenuItem[]>([]) const [supportMenu, setSupportMenu] = useState<MenuItem[]>([])
const [settings, setSettings] = useState<StoreSettings | null>(null) const [settings, setSettings] = useState<StoreSettings | null>(null)
const brandName = settings?.brandName || 'HomAroma'
const translateMenuLabel = (item: MenuItem) => { const translateMenuLabel = (item: MenuItem) => {
const lang = localStorage.getItem('homaroma_lang') || 'en' const lang = localStorage.getItem('homaroma_lang') || 'en'
@@ -64,7 +65,7 @@ export default function Footer({ categories = [] }: FooterProps) {
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12"> <div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
<div className="space-y-6"> <div className="space-y-6">
<div className="text-2xl font-bold tracking-widest"> <div className="text-2xl font-bold tracking-widest">
HomAroma <span className="text-cyan-glow text-sm font-light italic">Essential</span> {brandName} <span className="text-cyan-glow text-sm font-light italic">Essential</span>
</div> </div>
<p className="text-slate-500 text-sm leading-relaxed"> <p className="text-slate-500 text-sm leading-relaxed">
{settings?.footerDescription || t('footer.catchphrase')} {settings?.footerDescription || t('footer.catchphrase')}
@@ -135,7 +136,7 @@ export default function Footer({ categories = [] }: FooterProps) {
<div className="pt-4"> <div className="pt-4">
<p className="text-[10px] text-slate-600 uppercase tracking-widest"> <p className="text-[10px] text-slate-600 uppercase tracking-widest">
{settings?.footerCopyrightText || `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`} {settings?.footerCopyrightText || `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`}
</p> </p>
</div> </div>
</div> </div>
+13 -2
View File
@@ -1,6 +1,6 @@
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { type Category } from '../types' import { API_BASE, type Category, type StoreSettings } from '../types'
import { ShoppingBag, Menu as MenuIcon, X, Heart, Globe, Check } from 'lucide-react' import { ShoppingBag, Menu as MenuIcon, X, Heart, Globe, Check } from 'lucide-react'
import { useCartStore } from '../store/cartStore' import { useCartStore } from '../store/cartStore'
import { useWishlistStore } from '../store/wishlistStore' import { useWishlistStore } from '../store/wishlistStore'
@@ -26,6 +26,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
const [hasAnnouncement, setHasAnnouncement] = useState(false) const [hasAnnouncement, setHasAnnouncement] = useState(false)
const [settings, setSettings] = useState<StoreSettings | null>(null)
const navigate = useNavigate() const navigate = useNavigate()
const { toggleCart, getCartCount } = useCartStore() const { toggleCart, getCartCount } = useCartStore()
@@ -74,6 +75,15 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
} }
}, [fetchMegaMenu]) }, [fetchMegaMenu])
useEffect(() => {
fetch(`${API_BASE}/settings`)
.then((response) => (response.ok ? response.json() : null))
.then((data: StoreSettings | null) => {
if (data) setSettings(data)
})
.catch((error) => console.error('Failed to fetch store settings', error))
}, [])
// Keep --navbar-bottom in sync so the fixed mega-menu dropdown sits flush below the navbar // Keep --navbar-bottom in sync so the fixed mega-menu dropdown sits flush below the navbar
useEffect(() => { useEffect(() => {
const updateNavBottom = () => { const updateNavBottom = () => {
@@ -112,6 +122,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
: (hasAnnouncement ? 'top-[32px] md:top-[40px]' : 'top-0') : (hasAnnouncement ? 'top-[32px] md:top-[40px]' : 'top-0')
const finalTopClassName = scrolled ? 'top-0' : computedOffset const finalTopClassName = scrolled ? 'top-0' : computedOffset
const brandName = settings?.brandName || 'HomAroma'
return ( return (
<> <>
@@ -127,7 +138,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
{/* Left: Logo */} {/* Left: Logo */}
<div className="flex items-center shrink-0"> <div className="flex items-center shrink-0">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-spa-forest uppercase italic shrink-0"> <Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-spa-forest uppercase italic shrink-0">
HomAroma <span className="hidden md:inline text-spa-stone text-xs md:text-sm not-italic font-light ml-1">Essential</span> {brandName} <span className="hidden md:inline text-spa-stone text-xs md:text-sm not-italic font-light ml-1">Essential</span>
</Link> </Link>
</div> </div>
+1 -1
View File
@@ -88,7 +88,7 @@ export default async function RootLayout({
<html lang="th" className="scroll-smooth"> <html lang="th" className="scroll-smooth">
<body className="min-h-screen bg-pop-bg text-pop-black font-sans antialiased"> <body className="min-h-screen bg-pop-bg text-pop-black font-sans antialiased">
<ClientProviders> <ClientProviders>
<Navbar categories={categories} initialMenu={initialMenu} /> <Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
<main className="pt-[72px]"> <main className="pt-[72px]">
{children} {children}
</main> </main>
+4 -3
View File
@@ -52,6 +52,7 @@ export default function Footer({
// Social links — use settings if available, fallback to placeholder buttons // Social links — use settings if available, fallback to placeholder buttons
const hasSocialLinks = settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl) const hasSocialLinks = settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl)
const brandName = settings?.brandName || 'HomAroma'
return ( return (
<footer className="border-t border-gray-200 bg-white px-6 py-20 text-pop-black"> <footer className="border-t border-gray-200 bg-white px-6 py-20 text-pop-black">
@@ -60,7 +61,7 @@ export default function Footer({
{/* Col 1 — Brand */} {/* Col 1 — Brand */}
<div className="space-y-6"> <div className="space-y-6">
<div className="text-2xl font-bold tracking-widest"> <div className="text-2xl font-bold tracking-widest">
HomAroma <span className="text-sm font-semibold italic text-pop-pink">Essential</span> {brandName} <span className="text-sm font-semibold italic text-pop-pink">Essential</span>
</div> </div>
<p className="text-sm leading-relaxed text-gray-600"> <p className="text-sm leading-relaxed text-gray-600">
{resolveLocalized(settings?.footerDescription, effectiveLang) || t('footer.catchphrase')} {resolveLocalized(settings?.footerDescription, effectiveLang) || t('footer.catchphrase')}
@@ -174,8 +175,8 @@ export default function Footer({
? settings.footerCopyrightText ? settings.footerCopyrightText
: settings.footerCopyrightText[effectiveLang] : settings.footerCopyrightText[effectiveLang]
|| settings.footerCopyrightText.en || settings.footerCopyrightText.en
|| `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`) || `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`)
: `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`} : `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`}
</p> </p>
</div> </div>
</div> </div>
+5 -2
View File
@@ -14,10 +14,12 @@ import { useLang } from '../hooks/useLang'
import { DesktopMegaMenu } from './navigation/DesktopMegaMenu' import { DesktopMegaMenu } from './navigation/DesktopMegaMenu'
import { MobileDrawer } from './navigation/MobileDrawer' import { MobileDrawer } from './navigation/MobileDrawer'
import type { MegaMenuPayload } from './navigation/menuTypes' import type { MegaMenuPayload } from './navigation/menuTypes'
import type { StoreSettings } from '@homaroma/types'
interface NavbarProps { interface NavbarProps {
categories?: Category[] categories?: Category[]
initialMenu?: MegaMenuPayload[] initialMenu?: MegaMenuPayload[]
settings?: StoreSettings | null
} }
const LANGUAGES = [ const LANGUAGES = [
@@ -60,7 +62,7 @@ function createFallbackMenu(categories: Category[]): MegaMenuPayload[] {
] ]
} }
export default function Navbar({ categories = [], initialMenu = [] }: NavbarProps) { export default function Navbar({ categories = [], initialMenu = [], settings = null }: NavbarProps) {
const [searchOpen, setSearchOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const [langSheetOpen, setLangSheetOpen] = useState(false) const [langSheetOpen, setLangSheetOpen] = useState(false)
@@ -143,6 +145,7 @@ export default function Navbar({ categories = [], initialMenu = [] }: NavbarProp
const iconButtonClass = 'rounded-full p-1.5 text-pop-black/65 transition-all hover:bg-pop-bg hover:text-pop-pink md:p-2' const iconButtonClass = 'rounded-full p-1.5 text-pop-black/65 transition-all hover:bg-pop-bg hover:text-pop-pink md:p-2'
const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0] const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0]
const brandName = settings?.brandName || 'HomAroma'
return ( return (
<> <>
@@ -161,7 +164,7 @@ export default function Navbar({ categories = [], initialMenu = [] }: NavbarProp
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
className="shrink-0 text-xl font-bold uppercase italic tracking-widest text-pop-black md:text-2xl" className="shrink-0 text-xl font-bold uppercase italic tracking-widest text-pop-black md:text-2xl"
> >
HomAroma <span className="hidden text-xs font-light not-italic text-pop-black/55 md:inline md:text-sm">Essential</span> {brandName} <span className="hidden text-xs font-light not-italic text-pop-black/55 md:inline md:text-sm">Essential</span>
</Link> </Link>
</div> </div>
+1
View File
@@ -95,6 +95,7 @@ export interface Hero {
export interface StoreSettings { export interface StoreSettings {
id: number id: number
brandName: string
shippingFee: number shippingFee: number
bankName: string bankName: string
bankAccountName: string bankAccountName: string