feat: site settings CMS — favicon, logo, footer, social, OG image
Adds a full settings dashboard at /hq-command/dashboard/settings so the
client can update favicon, logos, footer text, social links and OG image
without code changes — wired into the SiteSetting model from the previous
commit.
NEW
- src/lib/siteSettingsTypes.ts: pure types + defaults (client-safe import)
- src/lib/siteSettings.ts: server-only loader using the SiteSetting model
- /api/assets gains a "branding" flat scope that writes to /public/branding
- /hq-command/dashboard/settings/{actions.ts, page.tsx} with three tabs:
1. Branding — favicon, Apple touch icon, main logo, email logo, OG
image, theme color. Each field has helper text with recommended
size/format and live preview.
2. Footer — CTA banner, HQ address, legal links. Optional one-click
AI translation to IT, VEC, ES, DE.
3. Social — LinkedIn, Instagram, YouTube, contact email.
WIRED INTO LAYOUT
- src/app/[locale]/layout.tsx now uses generateMetadata + generateViewport
to pull favicon, OG image and theme color dynamically. Adds Twitter
Card metadata. Falls back to the default flux-logo when SiteSetting
table is empty.
- src/components/layout/Footer.tsx reads CTA/HQ/legal copy from DB,
supports per-locale overrides via translationsJson, and renders social
icons (LinkedIn / Instagram / YouTube / Mail) only for filled fields.
UX FOR THE EDITOR (David's "12-year-old test")
- Drop-zone uploaders next to URL inputs — paste-or-upload either way
- Live image previews next to every branding field
- "Saved — live in 60 seconds" inline confirmation, no extra modals
- Recommended sizes spelled out next to each field (e.g. "PNG, square,
minimum 512×512" for favicon)
- Tooltip explaining why each image is needed
NO SCHEMA CHANGES — uses the SiteSetting table created in the previous
commit. Existing rows untouched.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// src/lib/siteSettings.ts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Server-only loader for site-wide settings (branding, footer, social).
|
||||
// Reads the SiteSetting key-value table and merges with defaults.
|
||||
//
|
||||
// For pure types and defaults safe to import from client components,
|
||||
// use src/lib/siteSettingsTypes.ts instead.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import "server-only";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getLocalizedData } from "@/lib/i18nHelper";
|
||||
import {
|
||||
DEFAULT_BRANDING,
|
||||
DEFAULT_FOOTER,
|
||||
DEFAULT_SOCIAL,
|
||||
type BrandingSettings,
|
||||
type FooterSettings,
|
||||
type SocialSettings,
|
||||
} from "@/lib/siteSettingsTypes";
|
||||
|
||||
export {
|
||||
DEFAULT_BRANDING,
|
||||
DEFAULT_FOOTER,
|
||||
DEFAULT_SOCIAL,
|
||||
type BrandingSettings,
|
||||
type FooterSettings,
|
||||
type SocialSettings,
|
||||
};
|
||||
|
||||
function safeParse<T>(json: string | null | undefined, fallback: T): T {
|
||||
if (!json) return fallback;
|
||||
try {
|
||||
return JSON.parse(json) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function readSetting<T>(key: string, defaults: T, locale?: string): Promise<T> {
|
||||
try {
|
||||
const row = await prisma.siteSetting.findUnique({ where: { key } });
|
||||
if (!row) return defaults;
|
||||
|
||||
const localized = locale ? getLocalizedData(row, locale) : row;
|
||||
const value = safeParse<Partial<T>>(localized.valueJson, {} as Partial<T>);
|
||||
return { ...defaults, ...value };
|
||||
} catch (error) {
|
||||
console.error(`[siteSettings] Failed to read "${key}":`, error);
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export const getBranding = (locale?: string) =>
|
||||
readSetting<BrandingSettings>("branding", DEFAULT_BRANDING, locale);
|
||||
|
||||
export const getFooterSettings = (locale?: string) =>
|
||||
readSetting<FooterSettings>("footer", DEFAULT_FOOTER, locale);
|
||||
|
||||
export const getSocialLinks = (locale?: string) =>
|
||||
readSetting<SocialSettings>("social", DEFAULT_SOCIAL, locale);
|
||||
|
||||
export async function getAllSettings() {
|
||||
const [branding, footer, social] = await Promise.all([
|
||||
getBranding(),
|
||||
getFooterSettings(),
|
||||
getSocialLinks(),
|
||||
]);
|
||||
return { branding, footer, social };
|
||||
}
|
||||
Reference in New Issue
Block a user