Add editable localized marquee settings

This commit is contained in:
MOH
2026-03-08 22:40:01 +01:00
parent 8740a94611
commit 1d4c2f8e72
9 changed files with 608 additions and 5 deletions
+40
View File
@@ -34,6 +34,14 @@ export {
type ContactProtectionFormValues,
type PublicContactProtectionSettings,
} from "./contact-protection";
export {
MARQUEE_SETTINGS_KEY,
buildDefaultMarqueeSettings,
parseMarqueeSettingsValue,
splitMarqueeRowItems,
type MarqueeLocaleSettings,
type MarqueeSettings,
} from "./marquee-settings";
import {
DEFAULT_SITE_NAME,
SITE_NAME_KEY,
@@ -68,6 +76,12 @@ import {
type ContactProtectionFormValues,
type PublicContactProtectionSettings,
} from "./contact-protection";
import {
MARQUEE_SETTINGS_KEY,
buildDefaultMarqueeSettings,
parseMarqueeSettingsValue,
type MarqueeSettings,
} from "./marquee-settings";
export async function getMaintenanceMode(): Promise<boolean> {
try {
@@ -203,6 +217,32 @@ export async function updateContactProtectionSettings(
});
}
export async function getMarqueeSettings(): Promise<MarqueeSettings> {
try {
const config = await prisma.appConfig.findUnique({
where: { key: MARQUEE_SETTINGS_KEY },
select: { value: true },
});
return parseMarqueeSettingsValue(config?.value);
} catch {
return buildDefaultMarqueeSettings();
}
}
export async function updateMarqueeSettings(settings: MarqueeSettings): Promise<void> {
await prisma.appConfig.upsert({
where: { key: MARQUEE_SETTINGS_KEY },
update: {
value: JSON.stringify(settings),
},
create: {
key: MARQUEE_SETTINGS_KEY,
value: JSON.stringify(settings),
},
});
}
export async function getSiteSettingsMediaBindings(): Promise<SiteSettingsMediaBindings> {
try {
const usages = await prisma.mediaUsage.findMany({
+128
View File
@@ -0,0 +1,128 @@
export const MARQUEE_SETTINGS_KEY = "marquee_settings";
export type MarqueeLocaleSettings = {
row1: string;
row2: string;
row3: string;
row4: string;
};
export type MarqueeSettings = {
locales: {
ar: MarqueeLocaleSettings;
en: MarqueeLocaleSettings;
de: MarqueeLocaleSettings;
};
};
const DEFAULT_ROW_1 = [
"Next.js",
"React",
"WordPress",
"Docker",
"Technical SEO",
"Tailwind CSS",
"Google Analytics",
"Framer Motion",
"Cloudflare",
"Performance",
].join("\n");
const DEFAULT_ROW_2 = [
"TypeScript",
"Node.js",
"WooCommerce",
"Schema Markup",
"DevOps",
"Responsive Design",
"Core Web Vitals",
"Plausible Analytics",
"Design Systems",
"Server Infrastructure",
].join("\n");
const DEFAULT_ROW_3 = [
"JavaScript",
"REST APIs",
"Git",
"Linux",
"Search Console",
"Conversion Tracking",
"UI Engineering",
"Accessibility",
"Component Architecture",
"Web Performance",
].join("\n");
const DEFAULT_ROW_4 = [
"Frontend Strategy",
"CMS Integration",
"Motion Design",
"Content Architecture",
"E-Commerce",
"SEO Audits",
"Deployments",
"Scalable UI",
"Analytics Setup",
"Product Pages",
].join("\n");
function buildDefaultLocaleSettings(): MarqueeLocaleSettings {
return {
row1: DEFAULT_ROW_1,
row2: DEFAULT_ROW_2,
row3: DEFAULT_ROW_3,
row4: DEFAULT_ROW_4,
};
}
export function buildDefaultMarqueeSettings(): MarqueeSettings {
return {
locales: {
ar: buildDefaultLocaleSettings(),
en: buildDefaultLocaleSettings(),
de: buildDefaultLocaleSettings(),
},
};
}
function normalizeLocaleSettings(value: unknown, defaults: MarqueeLocaleSettings): MarqueeLocaleSettings {
const candidate = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
return {
row1: typeof candidate.row1 === "string" && candidate.row1.trim() ? candidate.row1.trim() : defaults.row1,
row2: typeof candidate.row2 === "string" && candidate.row2.trim() ? candidate.row2.trim() : defaults.row2,
row3: typeof candidate.row3 === "string" && candidate.row3.trim() ? candidate.row3.trim() : defaults.row3,
row4: typeof candidate.row4 === "string" && candidate.row4.trim() ? candidate.row4.trim() : defaults.row4,
};
}
export function parseMarqueeSettingsValue(rawValue?: string | null): MarqueeSettings {
const defaults = buildDefaultMarqueeSettings();
if (!rawValue) {
return defaults;
}
try {
const parsed = JSON.parse(rawValue) as { locales?: Record<string, unknown> };
const locales = parsed && typeof parsed === "object" ? parsed.locales : undefined;
return {
locales: {
ar: normalizeLocaleSettings(locales?.ar, defaults.locales.ar),
en: normalizeLocaleSettings(locales?.en, defaults.locales.en),
de: normalizeLocaleSettings(locales?.de, defaults.locales.de),
},
};
} catch {
return defaults;
}
}
export function splitMarqueeRowItems(rawValue: string): string[] {
return rawValue
.split(/\r?\n|,/)
.map((item) => item.trim())
.filter(Boolean);
}
+9 -1
View File
@@ -8,6 +8,7 @@ import {
ShieldAlert,
SwatchBook,
Tags,
Type,
type LucideIcon,
} from "lucide-react";
@@ -18,6 +19,7 @@ type RootNavigationCopy = {
portfolio: string;
media: string;
siteSettings: string;
marquee?: string;
smtp?: string;
contactProtection?: string;
};
@@ -33,7 +35,7 @@ export type RootNavItem = {
export function getRootNavigation(
copy: RootNavigationCopy,
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp",
active: "overview" | "maintenance" | "ui-kit" | "portfolio" | "media" | "site-settings" | "smtp" | "marquee",
smtpChild?: "settings" | "contact-protection",
portfolioChild?: "overview" | "projects" | "new-project" | "categories",
): RootNavItem[] {
@@ -68,6 +70,12 @@ export function getRootNavigation(
icon: Globe2,
active: active === "site-settings",
},
{
label: copy.marquee ?? "Marquee",
href: "/root/marquee",
icon: Type,
active: active === "marquee",
},
{
label: copy.smtp ?? "SMTP",
href: "/root/smtp",