import "server-only"; import { prisma } from "@/lib/db"; import { getEffectivePlan } from "@/lib/billing/subscription"; export interface ActiveBranding { brandName: string | null; logoUrl: string | null; primaryColor: string | null; // hex (#rrggbb) removePoweredBy: boolean; } /** * Resolve the white-label branding that should apply to the current request. * Only returns branding when an organization is active AND its plan includes * the `custom_branding` feature AND a branding row exists. Otherwise null * (the default Podcast Distribution AI look). */ export async function getActiveBranding( userId: string, activeOrgId?: string | null ): Promise { if (!activeOrgId) return null; const { plan } = await getEffectivePlan(userId, activeOrgId); if (!plan.features.includes("custom_branding")) return null; const b = await prisma.orgBranding.findUnique({ where: { organizationId: activeOrgId } }); if (!b) return null; return { brandName: b.brandName, logoUrl: b.logoUrl, primaryColor: b.primaryColor, removePoweredBy: b.removePoweredBy, }; } /** * Convert a #rrggbb hex color to an `"H S% L%"` triplet for the `--brand` CSS * variable (the app's accent is consumed as `hsl(var(--brand))`). Returns null * for anything that isn't a valid 6-digit hex. */ export function hexToHslTriplet(hex: string | null | undefined): string | null { if (!hex) return null; const m = /^#?([0-9a-fA-F]{6})$/.exec(hex.trim()); if (!m) return null; const int = parseInt(m[1], 16); const r = ((int >> 16) & 255) / 255; const g = ((int >> 8) & 255) / 255; const b = (int & 255) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const l = (max + min) / 2; let h = 0; let s = 0; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; default: h = (r - g) / d + 4; } h /= 6; } return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`; }