x
This commit is contained in:
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)
|
||||
|
||||
// Form State
|
||||
const [brandName, setBrandName] = useState('')
|
||||
const [shippingFee, setShippingFee] = useState<string>('')
|
||||
const [bankName, setBankName] = useState('')
|
||||
const [bankAccountName, setBankAccountName] = useState('')
|
||||
@@ -29,6 +30,7 @@ export default function SystemSettingsPage() {
|
||||
fetch(`${API_BASE}/settings`)
|
||||
.then(r => r.json())
|
||||
.then((data: StoreSettings) => {
|
||||
setBrandName(data.brandName || 'HomAroma')
|
||||
setShippingFee(data.shippingFee.toString())
|
||||
setBankName(data.bankName || '')
|
||||
setBankAccountName(data.bankAccountName || '')
|
||||
@@ -58,6 +60,7 @@ export default function SystemSettingsPage() {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
brandName,
|
||||
shippingFee: parseFloat(shippingFee) || 0,
|
||||
bankName,
|
||||
bankAccountName,
|
||||
@@ -208,6 +211,19 @@ export default function SystemSettingsPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Footer Description</label>
|
||||
<textarea
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "StoreSettings" ADD COLUMN "brandName" TEXT NOT NULL DEFAULT 'HomAroma';
|
||||
@@ -207,6 +207,7 @@ model Promotion {
|
||||
|
||||
model StoreSettings {
|
||||
id Int @id @default(1)
|
||||
brandName String @default("HomAroma")
|
||||
shippingFee Float @default(35)
|
||||
bankName String @default("KASIKORNBANK (KBANK)")
|
||||
bankAccountName String @default("HomAroma Co., Ltd.")
|
||||
|
||||
@@ -39,6 +39,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
brandName: t.String(),
|
||||
shippingFee: t.Number(),
|
||||
bankName: t.String(),
|
||||
bankAccountName: t.String(),
|
||||
|
||||
@@ -28,6 +28,12 @@ bun run dev
|
||||
|
||||
ถ้าไม่ตั้งค่า `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
|
||||
|
||||
- `bun run dev`
|
||||
|
||||
@@ -477,6 +477,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
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",
|
||||
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);
|
||||
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": {
|
||||
const { id, lang, title, content } = params as any;
|
||||
const payload: any = {};
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
const [exploreMenu, setExploreMenu] = useState<MenuItem[]>([])
|
||||
const [supportMenu, setSupportMenu] = useState<MenuItem[]>([])
|
||||
const [settings, setSettings] = useState<StoreSettings | null>(null)
|
||||
const brandName = settings?.brandName || 'HomAroma'
|
||||
|
||||
const translateMenuLabel = (item: MenuItem) => {
|
||||
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="space-y-6">
|
||||
<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>
|
||||
<p className="text-slate-500 text-sm leading-relaxed">
|
||||
{settings?.footerDescription || t('footer.catchphrase')}
|
||||
@@ -135,7 +136,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
|
||||
<div className="pt-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
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 { useCartStore } from '../store/cartStore'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
@@ -26,6 +26,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [hasAnnouncement, setHasAnnouncement] = useState(false)
|
||||
const [settings, setSettings] = useState<StoreSettings | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { toggleCart, getCartCount } = useCartStore()
|
||||
@@ -74,6 +75,15 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
const updateNavBottom = () => {
|
||||
@@ -112,6 +122,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
: (hasAnnouncement ? 'top-[32px] md:top-[40px]' : 'top-0')
|
||||
|
||||
const finalTopClassName = scrolled ? 'top-0' : computedOffset
|
||||
const brandName = settings?.brandName || 'HomAroma'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -127,7 +138,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
{/* Left: Logo */}
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export default async function RootLayout({
|
||||
<html lang="th" className="scroll-smooth">
|
||||
<body className="min-h-screen bg-pop-bg text-pop-black font-sans antialiased">
|
||||
<ClientProviders>
|
||||
<Navbar categories={categories} initialMenu={initialMenu} />
|
||||
<Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
|
||||
<main className="pt-[72px]">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@@ -52,6 +52,7 @@ export default function Footer({
|
||||
|
||||
// Social links — use settings if available, fallback to placeholder buttons
|
||||
const hasSocialLinks = settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl)
|
||||
const brandName = settings?.brandName || 'HomAroma'
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
<p className="text-sm leading-relaxed text-gray-600">
|
||||
{resolveLocalized(settings?.footerDescription, effectiveLang) || t('footer.catchphrase')}
|
||||
@@ -174,8 +175,8 @@ export default function Footer({
|
||||
? settings.footerCopyrightText
|
||||
: settings.footerCopyrightText[effectiveLang]
|
||||
|| settings.footerCopyrightText.en
|
||||
|| `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`)
|
||||
: `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`}
|
||||
|| `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`)
|
||||
: `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,12 @@ import { useLang } from '../hooks/useLang'
|
||||
import { DesktopMegaMenu } from './navigation/DesktopMegaMenu'
|
||||
import { MobileDrawer } from './navigation/MobileDrawer'
|
||||
import type { MegaMenuPayload } from './navigation/menuTypes'
|
||||
import type { StoreSettings } from '@homaroma/types'
|
||||
|
||||
interface NavbarProps {
|
||||
categories?: Category[]
|
||||
initialMenu?: MegaMenuPayload[]
|
||||
settings?: StoreSettings | null
|
||||
}
|
||||
|
||||
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 [mobileOpen, setMobileOpen] = 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 activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0]
|
||||
const brandName = settings?.brandName || 'HomAroma'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -161,7 +164,7 @@ export default function Navbar({ categories = [], initialMenu = [] }: NavbarProp
|
||||
onClick={() => setMobileOpen(false)}
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user