x
This commit is contained in:
1 parent
48aebb2ec9
commit
b693c7188c
8 files changed
+267
-4
No files matched your search
@@ -21,6 +21,7 @@ import SystemMaintenancePage from './pages/SystemMaintenancePage'
|
|||||||
import MainNavigationPage from './pages/menu-manager/MainNavigationPage'
|
import MainNavigationPage from './pages/menu-manager/MainNavigationPage'
|
||||||
import FooterExplorePage from './pages/menu-manager/FooterExplorePage'
|
import FooterExplorePage from './pages/menu-manager/FooterExplorePage'
|
||||||
import FooterSupportPage from './pages/menu-manager/FooterSupportPage'
|
import FooterSupportPage from './pages/menu-manager/FooterSupportPage'
|
||||||
|
import FooterBrandPage from './pages/menu-manager/FooterBrandPage'
|
||||||
import NewsletterSubscribersPage from './pages/NewsletterSubscribersPage'
|
import NewsletterSubscribersPage from './pages/NewsletterSubscribersPage'
|
||||||
import ArticlesPage from './pages/ArticlesPage'
|
import ArticlesPage from './pages/ArticlesPage'
|
||||||
import ArticleEditPage from './pages/ArticleEditPage'
|
import ArticleEditPage from './pages/ArticleEditPage'
|
||||||
@@ -78,6 +79,7 @@ function App() {
|
|||||||
<Route path="/menu-manager/main-navigation" element={<MainNavigationPage />} />
|
<Route path="/menu-manager/main-navigation" element={<MainNavigationPage />} />
|
||||||
<Route path="/menu-manager/footer-explore" element={<FooterExplorePage />} />
|
<Route path="/menu-manager/footer-explore" element={<FooterExplorePage />} />
|
||||||
<Route path="/menu-manager/footer-support" element={<FooterSupportPage />} />
|
<Route path="/menu-manager/footer-support" element={<FooterSupportPage />} />
|
||||||
|
<Route path="/menu-manager/footer-brand" element={<FooterBrandPage />} />
|
||||||
<Route path="/subscribers" element={<NewsletterSubscribersPage />} />
|
<Route path="/subscribers" element={<NewsletterSubscribersPage />} />
|
||||||
<Route path="/articles" element={<ArticlesPage />} />
|
<Route path="/articles" element={<ArticlesPage />} />
|
||||||
<Route path="/articles/:id" element={<ArticleEditPage />} />
|
<Route path="/articles/:id" element={<ArticleEditPage />} />
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const MENU_MANAGER_MENU = [
|
|||||||
{ icon: Layers, label: 'Main Navigation', to: '/menu-manager/main-navigation' },
|
{ icon: Layers, label: 'Main Navigation', to: '/menu-manager/main-navigation' },
|
||||||
{ icon: FileText, label: 'Footer Explore', to: '/menu-manager/footer-explore' },
|
{ icon: FileText, label: 'Footer Explore', to: '/menu-manager/footer-explore' },
|
||||||
{ icon: MessageSquare, label: 'Footer Support', to: '/menu-manager/footer-support' },
|
{ icon: MessageSquare, label: 'Footer Support', to: '/menu-manager/footer-support' },
|
||||||
|
{ icon: Store, label: 'Footer Brand', to: '/menu-manager/footer-brand' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const SETTINGS_MENU = [
|
const SETTINGS_MENU = [
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
import { API_BASE, type FooterBrandResponse } from '@homaroma/types'
|
||||||
|
import { AlertCircle, CheckCircle2, Image as ImageIcon, Link as LinkIcon, Loader2, Save } from 'lucide-react'
|
||||||
|
|
||||||
|
type SaveState = { type: 'success' | 'error'; message: string } | null
|
||||||
|
|
||||||
|
export default function FooterBrandPage() {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [saveState, setSaveState] = useState<SaveState>(null)
|
||||||
|
const [brandName, setBrandName] = useState('')
|
||||||
|
const [logoUrl, setLogoUrl] = useState('')
|
||||||
|
const [facebookUrl, setFacebookUrl] = useState('')
|
||||||
|
const [instagramUrl, setInstagramUrl] = useState('')
|
||||||
|
const [tiktokUrl, setTiktokUrl] = useState('')
|
||||||
|
const [description, setDescription] = useState<Record<string, string>>({ th: '', en: '' })
|
||||||
|
const [activeLanguage, setActiveLanguage] = useState<'th' | 'en'>('th')
|
||||||
|
const [version, setVersion] = useState<string | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(`${API_BASE}/settings/footer-brand`)
|
||||||
|
.then(async response => {
|
||||||
|
if (!response.ok) throw new Error(`Unable to load settings (${response.status})`)
|
||||||
|
return response.json() as Promise<FooterBrandResponse>
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (cancelled) return
|
||||||
|
setVersion(data.version)
|
||||||
|
setBrandName(data.brand.brandName || '')
|
||||||
|
setLogoUrl(data.brand.logoUrl || '')
|
||||||
|
setDescription({ th: '', en: '', ...(data.brand.description || {}) })
|
||||||
|
setFacebookUrl(data.brand.socialLinks?.facebookUrl || '')
|
||||||
|
setInstagramUrl(data.brand.socialLinks?.instagramUrl || '')
|
||||||
|
setTiktokUrl(data.brand.socialLinks?.tiktokUrl || '')
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (!cancelled) setSaveState({ type: 'error', message: error instanceof Error ? error.message : 'Unable to load footer brand settings.' })
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSave = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setSaving(true)
|
||||||
|
setSaveState(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/settings/footer-brand`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('homaroma_admin_token') || ''}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ brandName, logoUrl: logoUrl || null, description, facebookUrl: facebookUrl || null, instagramUrl: instagramUrl || null, tiktokUrl: tiktokUrl || null, expectedVersion: version }),
|
||||||
|
})
|
||||||
|
const result = await response.json() as FooterBrandResponse & { error?: string; code?: string }
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 409 && result.code === 'VERSION_CONFLICT') {
|
||||||
|
throw new Error('Footer Brand changed in another session. Reload the page before saving again.')
|
||||||
|
}
|
||||||
|
throw new Error(result.error || `Unable to save settings (${response.status})`)
|
||||||
|
}
|
||||||
|
setVersion(result.version)
|
||||||
|
setDescription({ th: '', en: '', ...(result.brand.description || {}) })
|
||||||
|
setSaveState({ type: 'success', message: 'Footer brand saved successfully.' })
|
||||||
|
} catch (error) {
|
||||||
|
setSaveState({ type: 'error', message: error instanceof Error ? error.message : 'Unable to save footer brand.' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <div className="flex items-center justify-center p-20" role="status" aria-label="Loading footer brand"><Loader2 className="w-8 h-8 text-cyan-glow animate-spin" /></div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl space-y-8 animate-fade-in">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h1 className="text-3xl font-bold">Footer Brand</h1>
|
||||||
|
<p className="text-slate-400 text-sm">Manage the first brand column shown in the website footer.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{saveState && (
|
||||||
|
<div role="status" className={`flex items-center gap-2 rounded-lg border px-4 py-3 text-sm ${saveState.type === 'success' ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-200' : 'border-red-400/30 bg-red-400/10 text-red-200'}`}>
|
||||||
|
{saveState.type === 'success' ? <CheckCircle2 className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
|
||||||
|
{saveState.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSave} className="space-y-8">
|
||||||
|
<section className="admin-card space-y-6 p-8">
|
||||||
|
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
|
||||||
|
<div className="rounded-lg bg-purple-500/10 p-2"><ImageIcon className="h-5 w-5 text-purple-200" /></div>
|
||||||
|
<div><h2 className="text-lg font-bold uppercase tracking-widest text-white">Brand identity</h2><p className="text-xs text-slate-500">Used by the footer and shared site brand.</p></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-2 md:col-span-2"><label htmlFor="footer-brand-name" className="text-xs font-bold uppercase tracking-widest text-slate-400">Brand name</label><input id="footer-brand-name" required value={brandName} onChange={event => setBrandName(event.target.value)} className="admin-input w-full" placeholder="HomAroma" /></div>
|
||||||
|
<div className="space-y-2 md:col-span-2"><label htmlFor="footer-logo-url" className="text-xs font-bold uppercase tracking-widest text-slate-400">Logo URL</label><input id="footer-logo-url" type="url" value={logoUrl} onChange={event => setLogoUrl(event.target.value)} className="admin-input w-full" placeholder="https://example.com/logo.png" />{logoUrl && <div className="mt-3 inline-flex min-h-16 items-center rounded-lg border border-white/10 bg-black/20 p-3"><img src={logoUrl} alt="Footer logo preview" className="max-h-12 max-w-56 object-contain" /></div>}</div>
|
||||||
|
<div className="space-y-2 md:col-span-2"><label className="text-xs font-bold uppercase tracking-widest text-slate-400">Footer description</label><div className="flex gap-2">{(['th', 'en'] as const).map(language => <button key={language} type="button" onClick={() => setActiveLanguage(language)} className={`rounded-lg px-3 py-1.5 text-xs font-bold uppercase ${activeLanguage === language ? 'bg-cyan-glow text-night-900' : 'bg-white/5 text-slate-400 hover:bg-white/10'}`}>{language}</button>)}</div><textarea id="footer-description" value={description[activeLanguage] || ''} onChange={event => setDescription(current => ({ ...current, [activeLanguage]: event.target.value }))} className="admin-input min-h-24 w-full resize-y" placeholder={activeLanguage === 'th' ? 'กลิ่นหอมที่ออกแบบมาเพื่อบ้านของคุณ' : 'Scents designed for your home.'} /><p className="text-[10px] text-slate-500">Descriptions are saved per language and rendered using the visitor's locale.</p></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="admin-card space-y-6 p-8">
|
||||||
|
<div className="flex items-center gap-3 border-b border-white/10 pb-4"><div className="rounded-lg bg-cyan-glow/10 p-2"><LinkIcon className="h-5 w-5 text-cyan-glow" /></div><div><h2 className="text-lg font-bold uppercase tracking-widest text-white">Social links</h2><p className="text-xs text-slate-500">Empty links are omitted from the public footer.</p></div></div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<div className="space-y-2"><label htmlFor="footer-facebook" className="text-xs font-bold uppercase tracking-widest text-slate-400">Facebook URL</label><input id="footer-facebook" type="url" value={facebookUrl} onChange={event => setFacebookUrl(event.target.value)} className="admin-input w-full" placeholder="https://facebook.com/..." /></div>
|
||||||
|
<div className="space-y-2"><label htmlFor="footer-instagram" className="text-xs font-bold uppercase tracking-widest text-slate-400">Instagram URL</label><input id="footer-instagram" type="url" value={instagramUrl} onChange={event => setInstagramUrl(event.target.value)} className="admin-input w-full" placeholder="https://instagram.com/..." /></div>
|
||||||
|
<div className="space-y-2"><label htmlFor="footer-tiktok" className="text-xs font-bold uppercase tracking-widest text-slate-400">TikTok URL</label><input id="footer-tiktok" type="url" value={tiktokUrl} onChange={event => setTiktokUrl(event.target.value)} className="admin-input w-full" placeholder="https://tiktok.com/@..." /></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="submit" disabled={saving} className="btn-primary ml-auto flex items-center gap-2 px-12 py-4 text-sm"><Save className="h-4 w-4" />{saving ? 'Saving…' : 'Save footer brand'}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,22 +3,112 @@ import prisma from "../lib/prisma";
|
|||||||
import { adminOnly } from "../middleware/auth";
|
import { adminOnly } from "../middleware/auth";
|
||||||
|
|
||||||
export const settingsRoute = new Elysia({ prefix: "/settings" })
|
export const settingsRoute = new Elysia({ prefix: "/settings" })
|
||||||
.get("/", async ({ set }) => {
|
.get("/", async ({ set, query }) => {
|
||||||
try {
|
try {
|
||||||
let settings = await prisma.storeSettings.findFirst();
|
let settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
settings = await prisma.storeSettings.create({
|
settings = await prisma.storeSettings.create({
|
||||||
data: {}
|
data: {},
|
||||||
|
include: { translations: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return settings;
|
const { translations, ...settingsData } = settings;
|
||||||
|
const requestedLang = typeof query?.lang === "string" ? query.lang : undefined;
|
||||||
|
const localizedDescription = requestedLang
|
||||||
|
? translations.find((translation) => translation.langCode === requestedLang)?.footerDescription
|
||||||
|
: undefined;
|
||||||
|
return localizedDescription ? { ...settingsData, footerDescription: localizedDescription } : settingsData;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Settings] Error:", err);
|
console.error("[Settings] Error:", err);
|
||||||
set.status = 500;
|
set.status = 500;
|
||||||
return { error: "Failed to fetch settings" };
|
return { error: "Failed to fetch settings" };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
/**
|
||||||
|
* Dedicated Footer Brand contract. This keeps the editable Brand block
|
||||||
|
* separate from footer link menus while reusing the existing StoreSettings
|
||||||
|
* columns and StoreSettingsTranslation rows.
|
||||||
|
*/
|
||||||
|
.get("/footer-brand", async ({ set }) => {
|
||||||
|
try {
|
||||||
|
const settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
|
||||||
|
const base = settings ?? await prisma.storeSettings.create({ data: {}, include: { translations: true } });
|
||||||
|
const descriptions: Record<string, string> = {};
|
||||||
|
for (const translation of base.translations ?? []) {
|
||||||
|
if (translation.footerDescription) descriptions[translation.langCode] = translation.footerDescription;
|
||||||
|
}
|
||||||
|
if (base.footerDescription && !descriptions.th) descriptions.th = base.footerDescription;
|
||||||
|
return {
|
||||||
|
version: base.updatedAt.toISOString(),
|
||||||
|
brand: {
|
||||||
|
brandName: base.brandName,
|
||||||
|
logoUrl: base.logoUrl ?? null,
|
||||||
|
description: descriptions,
|
||||||
|
socialLinks: {
|
||||||
|
facebookUrl: base.facebookUrl ?? null,
|
||||||
|
instagramUrl: base.instagramUrl ?? null,
|
||||||
|
tiktokUrl: base.tiktokUrl ?? null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Settings] Footer brand fetch error:", err);
|
||||||
|
set.status = 500;
|
||||||
|
return { error: "Failed to fetch footer brand" };
|
||||||
|
}
|
||||||
|
})
|
||||||
.use(adminOnly)
|
.use(adminOnly)
|
||||||
|
.patch("/footer-brand", async ({ body, set }) => {
|
||||||
|
try {
|
||||||
|
const payload = body as any;
|
||||||
|
const settings = await prisma.storeSettings.findFirst({ include: { translations: true } });
|
||||||
|
const current = settings ?? await prisma.storeSettings.create({ data: {}, include: { translations: true } });
|
||||||
|
const version = current.updatedAt.toISOString();
|
||||||
|
if (payload.expectedVersion && payload.expectedVersion !== version) {
|
||||||
|
set.status = 409;
|
||||||
|
return { error: "Footer brand has changed since it was read", code: "VERSION_CONFLICT", expectedVersion: payload.expectedVersion, currentVersion: version };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: any = {};
|
||||||
|
if (payload.brandName !== undefined) {
|
||||||
|
if (typeof payload.brandName !== "string" || !payload.brandName.trim()) throw new Error("brandName must be a non-empty string");
|
||||||
|
updates.brandName = payload.brandName.trim();
|
||||||
|
}
|
||||||
|
for (const field of ["logoUrl", "facebookUrl", "instagramUrl", "tiktokUrl"]) {
|
||||||
|
if (payload[field] !== undefined) {
|
||||||
|
if (payload[field] !== null && typeof payload[field] !== "string") throw new Error(`${field} must be a string or null`);
|
||||||
|
updates[field] = payload[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const description = payload.description ?? payload.footerDescription;
|
||||||
|
if (description !== undefined && (typeof description !== "object" || Array.isArray(description))) throw new Error("description must be an object keyed by locale");
|
||||||
|
if (payload.dryRun) {
|
||||||
|
return { dryRun: true, version, wouldUpdate: { ...updates, description: description ?? undefined } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = Object.keys(updates).length
|
||||||
|
? await prisma.storeSettings.update({ where: { id: current.id }, data: updates })
|
||||||
|
: current;
|
||||||
|
if (description) {
|
||||||
|
for (const [langCode, value] of Object.entries(description)) {
|
||||||
|
if (typeof langCode !== "string" || typeof value !== "string") throw new Error("description values must be strings");
|
||||||
|
const existing = await prisma.storeSettingsTranslation.findUnique({ where: { storeSettingsId_langCode: { storeSettingsId: current.id, langCode } } });
|
||||||
|
if (existing) await prisma.storeSettingsTranslation.update({ where: { id: existing.id }, data: { footerDescription: value } });
|
||||||
|
else await prisma.storeSettingsTranslation.create({ data: { storeSettingsId: current.id, langCode, footerDescription: value } });
|
||||||
|
if (langCode === "th") await prisma.storeSettings.update({ where: { id: current.id }, data: { footerDescription: value } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await prisma.storeSettings.findUnique({ where: { id: current.id }, include: { translations: true } });
|
||||||
|
const descriptions: Record<string, string> = {};
|
||||||
|
for (const translation of result?.translations ?? []) if (translation.footerDescription) descriptions[translation.langCode] = translation.footerDescription;
|
||||||
|
if (result?.footerDescription && !descriptions.th) descriptions.th = result.footerDescription;
|
||||||
|
return { success: true, version: result?.updatedAt.toISOString(), brand: { brandName: result?.brandName, logoUrl: result?.logoUrl ?? null, description: descriptions, socialLinks: { facebookUrl: result?.facebookUrl ?? null, instagramUrl: result?.instagramUrl ?? null, tiktokUrl: result?.tiktokUrl ?? null } } };
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[Settings] Footer brand update error:", err);
|
||||||
|
set.status = 400;
|
||||||
|
return { error: err?.message || "Failed to update footer brand" };
|
||||||
|
}
|
||||||
|
}, { body: t.Any() })
|
||||||
.put("/", async ({ body, set }) => {
|
.put("/", async ({ body, set }) => {
|
||||||
try {
|
try {
|
||||||
const data = body as any;
|
const data = body as any;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Backend target used by the MCP server. Override per environment as needed.
|
||||||
|
MCP_BACKEND_URL=https://api.ddc.kittisak.com/api
|
||||||
@@ -33,6 +33,7 @@ bun run dev
|
|||||||
- `update_brand_name` เปลี่ยนชื่อแบรนด์กลางที่ใช้ใน header/footer ของ `apps/spa` และ `apps/website`
|
- `update_brand_name` เปลี่ยนชื่อแบรนด์กลางที่ใช้ใน header/footer ของ `apps/spa` และ `apps/website`
|
||||||
- `update_store_settings_translation` อัปเดตข้อความแปลของ store settings
|
- `update_store_settings_translation` อัปเดตข้อความแปลของ store settings
|
||||||
- `update_footer_content` อัปเดตคำอธิบายแบรนด์และ social links เท่านั้น
|
- `update_footer_content` อัปเดตคำอธิบายแบรนด์และ social links เท่านั้น
|
||||||
|
- `get_footer_brand` / `update_footer_brand` อ่านและแก้ Footer Brand block โดยตรง (brand name, logo, คำอธิบายแยกภาษา, social links) พร้อม `dryRun` และ `expectedVersion`
|
||||||
|
|
||||||
## Footer Menu LLM Workflow
|
## Footer Menu LLM Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "get_footer_brand",
|
||||||
|
description: "Read the editable Footer Brand block (name, logo, localized description, and social links). Pass the returned version to update_footer_brand to prevent stale writes.",
|
||||||
|
inputSchema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_footer_brand",
|
||||||
|
description: "Update Footer Brand fields without replacing footer menus. Supports localized description, dryRun preview, and expectedVersion conflict protection.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
brandName: { type: "string" },
|
||||||
|
logoUrl: { type: ["string", "null"] },
|
||||||
|
description: { type: "object", description: "Locale-to-description map, e.g. { th: '...', en: '...' }" },
|
||||||
|
socialLinks: { type: "object", properties: { facebookUrl: { type: ["string", "null"] }, instagramUrl: { type: ["string", "null"] }, tiktokUrl: { type: ["string", "null"] } } },
|
||||||
|
dryRun: { type: "boolean" },
|
||||||
|
expectedVersion: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "update_brand_name",
|
name: "update_brand_name",
|
||||||
description: "Update the brand name used across website and SPA headers, footers, and brand displays.",
|
description: "Update the brand name used across website and SPA headers, footers, and brand displays.",
|
||||||
@@ -1413,6 +1433,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
const res = await api.put("/settings", mergedSettings);
|
const res = await api.put("/settings", mergedSettings);
|
||||||
return { content: [{ type: "text", text: "Theme tokens updated successfully:\\n" + JSON.stringify(res.data.themeTokens, null, 2) }] };
|
return { content: [{ type: "text", text: "Theme tokens updated successfully:\\n" + JSON.stringify(res.data.themeTokens, null, 2) }] };
|
||||||
}
|
}
|
||||||
|
case "get_footer_brand": {
|
||||||
|
const res = await api.get("/settings/footer-brand");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||||
|
}
|
||||||
|
case "update_footer_brand": {
|
||||||
|
const payload: any = { ...params };
|
||||||
|
const social = payload.socialLinks;
|
||||||
|
delete payload.socialLinks;
|
||||||
|
if (social && typeof social === "object") Object.assign(payload, social);
|
||||||
|
const res = await api.patch("/settings/footer-brand", payload);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, tool: "update_footer_brand", ...res.data }, null, 2) }] };
|
||||||
|
}
|
||||||
case "update_brand_name": {
|
case "update_brand_name": {
|
||||||
const { brandName } = params as any;
|
const { brandName } = params as any;
|
||||||
const currentSettingsRes = await api.get("/settings");
|
const currentSettingsRes = await api.get("/settings");
|
||||||
|
|||||||
@@ -195,6 +195,22 @@ export interface StoreSettings {
|
|||||||
scrollRevealDuration?: number | null
|
scrollRevealDuration?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FooterBrand {
|
||||||
|
brandName: string
|
||||||
|
logoUrl?: string | null
|
||||||
|
description: Record<string, string>
|
||||||
|
socialLinks: {
|
||||||
|
facebookUrl?: string | null
|
||||||
|
instagramUrl?: string | null
|
||||||
|
tiktokUrl?: string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterBrandResponse {
|
||||||
|
version: string
|
||||||
|
brand: FooterBrand
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
id: number
|
id: number
|
||||||
label: string
|
label: string
|
||||||
|
|||||||
Reference in new issue
Block a user