b9a744bdbc
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.
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
// 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 };
|
|
}
|