Site Settings -> Branding now supports a Primary/Secondary/Accent brand color system applied site-wide (buttons, nav, hover states, links) and to outgoing email header/CTA colors, plus a favicon upload alongside the existing logo upload, a live preview panel (website/email x desktop/mobile), and logo-based color suggestions. The setup wizard's Branding step got the same treatment. Fixes two related bugs found along the way: the setup wizard's logo/favicon upload was missing its auth token, and a static favicon.ico in Next's special app/ convention path was silently overriding the dynamic one. Also replaces every "Hope Events"/"Hope Family Church" default (org name, email subjects, WhatsApp messages, report metadata, API docs) with a neutral "Cross Code" placeholder, and the optional legal settings (operator name, IO details, website URL, effective date) with obviously-generic placeholders instead of defaulting to real personal/organisational details -- since this platform is deployed for multiple organisations. Adds SETTINGS.md documenting every setting's default behaviour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
8.1 KiB
TypeScript
172 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import React, { useMemo, useState } from "react";
|
|
import { Monitor, Smartphone, RefreshCw } from "lucide-react";
|
|
import { buildBrandScale, buildSecondaryTokens, buildAccentTokens, contrastForeground, isValidHex } from "@/lib/colorScale";
|
|
|
|
type View = "website" | "email";
|
|
type Viewport = "desktop" | "mobile";
|
|
|
|
/**
|
|
* Hand-built mockup (not an iframe of the real site) so it can reflect
|
|
* in-progress, unsaved color-picker values instantly — the real site's CSS
|
|
* variables only update once the admin actually saves (see layout.tsx's SSR
|
|
* style tag), and this preview must not leak into the surrounding admin
|
|
* page's own theme while the admin is still experimenting with colors.
|
|
*/
|
|
export function BrandingPreviewPanel({
|
|
primaryColor, secondaryColor, accentColor, logoSrc, orgName, orgTagline,
|
|
}: {
|
|
primaryColor: string;
|
|
secondaryColor: string;
|
|
accentColor: string;
|
|
logoSrc: string | null;
|
|
orgName: string;
|
|
orgTagline?: string;
|
|
}) {
|
|
const [view, setView] = useState<View>("website");
|
|
const [viewport, setViewport] = useState<Viewport>("desktop");
|
|
|
|
const safePrimary = isValidHex(primaryColor) ? primaryColor : "#4F46E5";
|
|
const safeSecondary = isValidHex(secondaryColor) ? secondaryColor : "#8B5CF6";
|
|
const safeAccent = isValidHex(accentColor) ? accentColor : "#EC4899";
|
|
|
|
const scale = useMemo(() => buildBrandScale(safePrimary), [safePrimary]);
|
|
const secondary = useMemo(() => buildSecondaryTokens(safeSecondary), [safeSecondary]);
|
|
const accent = useMemo(() => buildAccentTokens(safeAccent), [safeAccent]);
|
|
const primaryFg = useMemo(() => contrastForeground(safePrimary), [safePrimary]);
|
|
|
|
return (
|
|
<div className="border rounded-xl bg-white shadow-sm overflow-hidden">
|
|
<div className="flex items-center justify-between px-4 py-3 border-b bg-gray-50">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-900">Live Preview</h3>
|
|
<p className="text-xs text-gray-500">See how your branding looks across the site.</p>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="flex items-center rounded-lg border bg-white overflow-hidden">
|
|
<button type="button" onClick={() => setView("website")}
|
|
className={`px-2.5 py-1.5 text-xs font-medium ${view === "website" ? "bg-brand-600 text-white" : "text-gray-600 hover:bg-gray-100"}`}>
|
|
Website
|
|
</button>
|
|
<button type="button" onClick={() => setView("email")}
|
|
className={`px-2.5 py-1.5 text-xs font-medium ${view === "email" ? "bg-brand-600 text-white" : "text-gray-600 hover:bg-gray-100"}`}>
|
|
Email
|
|
</button>
|
|
</div>
|
|
{view === "website" && (
|
|
<div className="flex items-center rounded-lg border bg-white overflow-hidden">
|
|
<button type="button" onClick={() => setViewport("desktop")} title="Desktop"
|
|
className={`p-1.5 ${viewport === "desktop" ? "bg-gray-200" : "text-gray-500 hover:bg-gray-100"}`}>
|
|
<Monitor className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button type="button" onClick={() => setViewport("mobile")} title="Mobile"
|
|
className={`p-1.5 ${viewport === "mobile" ? "bg-gray-200" : "text-gray-500 hover:bg-gray-100"}`}>
|
|
<Smartphone className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-gray-100 flex justify-center overflow-x-auto">
|
|
{view === "website" ? (
|
|
<div
|
|
className="bg-white rounded-lg shadow-md overflow-hidden transition-all"
|
|
style={{ width: viewport === "mobile" ? 320 : "100%", maxWidth: viewport === "mobile" ? 320 : 640 }}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 px-3 py-2.5 border-b">
|
|
{logoSrc ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img src={logoSrc} alt="Logo" className="h-6 w-6 object-contain rounded-sm" />
|
|
) : (
|
|
<div className="h-6 w-6 rounded-sm shrink-0" style={{ background: scale["600"] }} />
|
|
)}
|
|
<span className="text-sm font-bold truncate" style={{ color: scale["600"] }}>{orgName || "Your Organisation"}</span>
|
|
<div className="ml-auto flex items-center gap-2 text-[10px] text-gray-400">
|
|
<span>Home</span><span>Events</span><span>Contact</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hero */}
|
|
<div className="px-4 py-6 text-center" style={{ background: `hsl(${secondary.bg})` }}>
|
|
<p className="text-base font-bold text-gray-900">Welcome to {orgName || "Your Organisation"}</p>
|
|
<p className="text-[11px] text-gray-500 mt-1 mb-4">Experience unforgettable moments.</p>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span
|
|
className="text-[11px] font-semibold px-3 py-1.5 rounded-lg"
|
|
style={{ background: scale["600"], color: primaryFg }}
|
|
>
|
|
View Events
|
|
</span>
|
|
<span
|
|
className="text-[11px] font-semibold px-3 py-1.5 rounded-lg border bg-white"
|
|
style={{ borderColor: `hsl(${secondary.fg})`, color: `hsl(${secondary.fg})` }}
|
|
>
|
|
My Dashboard
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming events */}
|
|
<div className="px-4 py-4">
|
|
<p className="text-xs font-semibold text-gray-900 mb-2">Upcoming events</p>
|
|
<div className="space-y-2">
|
|
{["Sunday Service", "Community Outreach"].map((title) => (
|
|
<div key={title} className="flex items-center gap-2 border rounded-lg p-2">
|
|
<span
|
|
className="w-2 h-2 rounded-full shrink-0"
|
|
style={{ background: `hsl(${accent.fg})` }}
|
|
/>
|
|
<span className="text-[11px] text-gray-700 truncate">{title}</span>
|
|
<span
|
|
className="ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded"
|
|
style={{ background: `hsl(${accent.bg})`, color: `hsl(${accent.fg})` }}
|
|
>
|
|
New
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer — fixed dark neutral, not brand-driven */}
|
|
<div className="px-4 py-4 bg-slate-900 text-center">
|
|
<p className="text-[10px] text-slate-400">© {new Date().getFullYear()} {orgName || "Your Organisation"}. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-lg shadow-md overflow-hidden w-full max-w-md">
|
|
<div
|
|
className="px-6 py-8 text-center"
|
|
style={{ background: `linear-gradient(135deg, ${scale["600"]} 0%, #2d5287 100%)` }}
|
|
>
|
|
<p className="text-lg font-extrabold text-white">{orgName || "Your Organisation"}</p>
|
|
{orgTagline && <p className="text-xs text-white/90 mt-1">{orgTagline}</p>}
|
|
</div>
|
|
<div className="px-6 py-6">
|
|
<p className="text-sm font-bold text-gray-900 mb-1">Sample notification</p>
|
|
<div className="h-2 bg-gray-100 rounded w-full mb-1.5" />
|
|
<div className="h-2 bg-gray-100 rounded w-5/6 mb-4" />
|
|
<div className="flex justify-center">
|
|
<span
|
|
className="text-xs font-bold px-6 py-2.5 rounded-lg"
|
|
style={{ background: scale["600"], color: primaryFg }}
|
|
>
|
|
Call to action
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="px-4 py-2 border-t bg-gray-50 flex items-center gap-1.5 text-[11px] text-gray-400">
|
|
<RefreshCw className="w-3 h-3" />
|
|
Updates live as you change colors — save to apply site-wide.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|