import "server-only";
import { DEFAULT_ACCENT, isHexColor } from "@/lib/branding-colors";
import { prisma } from "@/lib/db";
import { QrRequestError } from "@/lib/qr/type-module";
export type Branding = {
appName: string;
footerText: string;
accentColor: string;
logoUploadId: string | null;
faviconUploadId: string | null;
/** MIME-Typ des Favicons — der `` braucht ihn. */
faviconMimeType: string | null;
};
export const DEFAULT_BRANDING: Branding = {
appName: "QR-Code-Generator",
footerText: "statische und dynamische QR-Codes",
accentColor: DEFAULT_ACCENT,
logoUploadId: null,
faviconUploadId: null,
faviconMimeType: null,
};
export const MAX_APP_NAME = 60;
export const MAX_FOOTER_TEXT = 120;
/**
* Bewusst **ohne** Zwischenspeicher.
*
* Ein erster Versuch hielt den Wert 15 Sekunden lang fest, um die Abfrage je
* Seitenaufruf zu sparen. Das ging schief: Die API-Route und das Layout
* laufen in getrennten Modulinstanzen, das Verwerfen beim Speichern erreichte
* die andere Instanz also gar nicht. Nach dem Speichern blieb die Oberfläche
* unverändert — der schlechteste denkbare Eindruck bei einer Einstellung, die
* man ansieht.
*
* Es bleibt bei einer Abfrage auf den Primärschlüssel einer Tabelle mit einer
* einzigen Zeile. Das ist zu verschmerzen; jede Seite fragt ohnehin mehr ab.
*/
export async function getBranding(): Promise {
let wert = DEFAULT_BRANDING;
try {
const zeile = await prisma.branding.findUnique({
where: { id: "branding" },
include: {
faviconUpload: { select: { mimeType: true } },
},
});
if (zeile) {
wert = {
appName: zeile.appName,
footerText: zeile.footerText,
accentColor: isHexColor(zeile.accentColor)
? zeile.accentColor
: DEFAULT_ACCENT,
logoUploadId: zeile.logoUploadId,
faviconUploadId: zeile.faviconUploadId,
faviconMimeType: zeile.faviconUpload?.mimeType ?? null,
};
}
} catch (error) {
// Ein Problem mit der Datenbank darf nicht dazu führen, dass gar keine
// Seite mehr erscheint — dann eben im Auslieferungszustand.
console.error("Erscheinungsbild konnte nicht geladen werden:", error);
}
return wert;
}
export type BrandingUpdate = {
appName?: string;
footerText?: string;
accentColor?: string;
/** `null` entfernt das Bild, `undefined` lässt es unverändert. */
logoUploadId?: string | null;
faviconUploadId?: string | null;
};
/**
* Schreibt das Erscheinungsbild und räumt ersetzte Bilder weg.
*
* Ohne das Aufräumen bliebe jedes einmal hochgeladene Logo für immer in der
* Datenbank liegen: Es gehört zu keinem Code, und die Aufräumroutine für
* herrenlose Dateien lässt „branding" bewusst in Ruhe.
*/
export async function saveBranding(update: BrandingUpdate): Promise {
if (update.accentColor !== undefined && !isHexColor(update.accentColor)) {
throw new QrRequestError(
"Die Farbe muss als Hex-Wert angegeben werden, z. B. #4F46E5.",
);
}
const vorher = await prisma.branding.findUnique({
where: { id: "branding" },
select: { logoUploadId: true, faviconUploadId: true },
});
const daten = {
...(update.appName !== undefined ? { appName: update.appName.trim() } : {}),
...(update.footerText !== undefined
? { footerText: update.footerText.trim() }
: {}),
...(update.accentColor !== undefined
? { accentColor: update.accentColor.trim().toUpperCase() }
: {}),
...(update.logoUploadId !== undefined
? { logoUploadId: update.logoUploadId }
: {}),
...(update.faviconUploadId !== undefined
? { faviconUploadId: update.faviconUploadId }
: {}),
};
await prisma.branding.upsert({
where: { id: "branding" },
update: daten,
create: { id: "branding", ...daten },
});
// Die ersetzten Dateien erst danach löschen — solange die neue Zeile nicht
// steht, wird noch die alte gebraucht.
const abgeloest = [
update.logoUploadId !== undefined &&
vorher?.logoUploadId &&
vorher.logoUploadId !== update.logoUploadId &&
vorher.logoUploadId,
update.faviconUploadId !== undefined &&
vorher?.faviconUploadId &&
vorher.faviconUploadId !== update.faviconUploadId &&
vorher.faviconUploadId,
].filter((id): id is string => typeof id === "string");
if (abgeloest.length > 0) {
await prisma.upload
.deleteMany({ where: { id: { in: abgeloest } } })
.catch(() => undefined);
}
return getBranding();
}