"use client"; import React, { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { apiFetch, API_BASE } from "@/lib/api"; import { ColorPickerField } from "@/components/admin/ColorPickerField"; import { BrandingPreviewPanel } from "@/components/admin/BrandingPreviewPanel"; import { extractDominantColors } from "@/lib/extractColors"; type Step = 1 | 2 | 3 | 4 | 5 | 6; const TOTAL_STEPS = 5; // steps 1-5; step 6 is the done screen function ProgressBar({ step }: { step: Step }) { return (
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map((s) => (
))}
); } function Field({ label, required, hint, children, }: { label: string; required?: boolean; hint?: string; children: React.ReactNode; }) { return (
{children} {hint &&

{hint}

}
); } const inputCls = "w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400"; export default function SetupPage() { const router = useRouter(); const [step, setStep] = useState(1); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [setupToken, setSetupToken] = useState(null); // SMTP test state const [smtpTesting, setSmtpTesting] = useState(false); const [smtpTestResult, setSmtpTestResult] = useState<{ ok: boolean; message: string } | null>(null); // Step 1 — Organisation const [orgName, setOrgName] = useState(""); const [orgTagline, setOrgTagline] = useState(""); const [orgEmail, setOrgEmail] = useState(""); const [orgPhone, setOrgPhone] = useState(""); const [orgAddress, setOrgAddress] = useState(""); const [appBaseUrl, setAppBaseUrl] = useState(""); // Step 2 — Admin account const [adminName, setAdminName] = useState(""); const [adminEmail, setAdminEmail] = useState(""); const [adminPassword, setAdminPassword] = useState(""); const [adminConfirm, setAdminConfirm] = useState(""); // Step 3 — Branding const [primaryColor, setPrimaryColor] = useState("#4F46E5"); const [secondaryColor, setSecondaryColor] = useState("#8B5CF6"); const [accentColor, setAccentColor] = useState("#EC4899"); const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(null); const [faviconFile, setFaviconFile] = useState(null); const [suggestedColors, setSuggestedColors] = useState([]); const [notifEmail, setNotifEmail] = useState(""); // Step 4 — Legal const [legalOperatorName, setLegalOperatorName] = useState(""); const [legalWebsiteUrl, setLegalWebsiteUrl] = useState(""); const [legalEffectiveDate, setLegalEffectiveDate] = useState(""); const [legalIoName, setLegalIoName] = useState(""); const [legalIoEmail, setLegalIoEmail] = useState(""); // Step 5 — Email / SMTP (skippable) const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState("587"); const [smtpSecure, setSmtpSecure] = useState(false); const [smtpFrom, setSmtpFrom] = useState(""); const [smtpUser, setSmtpUser] = useState(""); const [smtpPass, setSmtpPass] = useState(""); // Check setup isn't already done useEffect(() => { apiFetch<{ needsSetup: boolean }>("/api/settings/needs-setup") .then((d) => { if (!d?.needsSetup) router.replace("/login"); }) .catch(() => {}); }, []); const next = () => setStep((s) => Math.min(s + 1, 6) as Step); const back = () => { setError(null); setStep((s) => Math.max(s - 1, 1) as Step); }; const handleLogoChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setLogoFile(file); const preview = URL.createObjectURL(file); setLogoPreview(preview); setSuggestedColors(await extractDominantColors(preview)); }; const handleFaviconChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setFaviconFile(file); }; // ── Step validators ─────────────────────────────────────────────────────── const handleStep1 = (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!orgName.trim()) { setError("Organisation name is required."); return; } next(); }; const handleStep2 = async (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!adminName.trim()) { setError("Your name is required."); return; } if (!adminEmail.trim()) { setError("Email is required."); return; } if (adminPassword.length < 8) { setError("Password must be at least 8 characters."); return; } if (adminPassword !== adminConfirm) { setError("Passwords do not match."); return; } setLoading(true); try { const res = await apiFetch<{ token: string }>("/api/setup/register", { method: "POST", body: { adminName, adminEmail, adminPassword }, }); setSetupToken(res.token); next(); } catch (e: any) { setError(e?.message || "Failed to create admin account. Please try again."); } finally { setLoading(false); } }; const handleStep3 = (e: React.FormEvent) => { e.preventDefault(); setError(null); next(); }; const handleStep4 = (e: React.FormEvent) => { e.preventDefault(); setError(null); next(); }; // ── SMTP connection test ────────────────────────────────────────────────── const handleTestSmtp = async () => { setSmtpTesting(true); setSmtpTestResult(null); try { const res = await apiFetch<{ ok: boolean; message?: string }>("/api/settings/test-smtp", { method: "POST", authToken: setupToken, body: { host: smtpHost.trim(), port: smtpPort.trim(), secure: smtpSecure, from: smtpFrom.trim() || undefined, user: smtpUser.trim() || undefined, pass: smtpPass || undefined, }, }); setSmtpTestResult({ ok: true, message: res.message || "Connection successful." }); } catch (e: any) { setSmtpTestResult({ ok: false, message: e?.message || "Connection failed." }); } finally { setSmtpTesting(false); } }; // ── Finish (called from step 5 or skip) ────────────────────────────────── const handleFinish = async (skipSmtp = false) => { setError(null); setLoading(true); try { // The admin account already exists by this point (created in step 2), // so the branding-asset upload endpoints require admin auth — the // setupToken from /api/setup/register covers that. let logoUrl = ""; if (logoFile) { const fd = new FormData(); fd.append("image", logoFile); const uploadRes = await fetch(`${API_BASE}/api/uploads/logo`, { method: "POST", headers: setupToken ? { Authorization: `Bearer ${setupToken}` } : undefined, body: fd, }); if (uploadRes.ok) { const up = await uploadRes.json(); logoUrl = up.url || ""; } else { throw new Error("Logo upload failed. Please try again."); } } let faviconUrl = ""; if (faviconFile) { const fd = new FormData(); fd.append("image", faviconFile); const uploadRes = await fetch(`${API_BASE}/api/uploads/favicon`, { method: "POST", headers: setupToken ? { Authorization: `Bearer ${setupToken}` } : undefined, body: fd, }); if (uploadRes.ok) { const up = await uploadRes.json(); faviconUrl = up.url || ""; } else { throw new Error("Favicon upload failed. Please try again."); } } const settings: Record = { org_name: orgName.trim(), primary_color: primaryColor, secondary_color: secondaryColor, accent_color: accentColor, }; if (orgTagline.trim()) settings.org_tagline = orgTagline.trim(); if (orgEmail.trim()) settings.org_email = orgEmail.trim(); if (orgPhone.trim()) settings.org_phone = orgPhone.trim(); if (orgAddress.trim()) settings.org_address = orgAddress.trim(); if (appBaseUrl.trim()) settings.app_base_url = appBaseUrl.trim(); if (notifEmail.trim()) settings.reg_notification_emails = notifEmail.trim(); if (logoUrl) settings.logo_url = logoUrl; if (faviconUrl) settings.favicon_url = faviconUrl; // Legal if (legalOperatorName.trim()) settings.legal_operator_name = legalOperatorName.trim(); if (legalWebsiteUrl.trim()) settings.legal_website_url = legalWebsiteUrl.trim(); if (legalEffectiveDate.trim()) settings.legal_effective_date = legalEffectiveDate.trim(); if (legalIoName.trim()) settings.legal_io_name = legalIoName.trim(); if (legalIoEmail.trim()) settings.legal_io_email = legalIoEmail.trim(); // SMTP (only if not skipped and host provided) if (!skipSmtp && smtpHost.trim()) { settings.smtp_host = smtpHost.trim(); settings.smtp_port = smtpPort.trim(); settings.smtp_secure = smtpSecure ? "true" : "false"; if (smtpFrom.trim()) settings.smtp_from = smtpFrom.trim(); if (smtpUser.trim()) settings.smtp_user = smtpUser.trim(); if (smtpPass.trim()) settings.smtp_pass = smtpPass.trim(); } await apiFetch("/api/setup", { method: "POST", authToken: setupToken, body: { settings }, }); next(); // → step 6 (done) } catch (e: any) { setError(e?.message || "Setup failed. Please try again."); } finally { setLoading(false); } }; return (
{/* Heading */}

Welcome

Let's set up your events platform

{step < 6 && } {error && (
{error}
)} {/* ── Step 1: Organisation ──────────────────────────── */} {step === 1 && (

Organisation details

These appear throughout the site and in emails.

setOrgName(e.target.value)} autoFocus /> setOrgTagline(e.target.value)} />
setOrgEmail(e.target.value)} /> setOrgPhone(e.target.value)} />
setOrgAddress(e.target.value)} /> setAppBaseUrl(e.target.value)} />
)} {/* ── Step 2: Admin account ─────────────────────────── */} {step === 2 && (

Create admin account

Your primary administrator login.

setAdminName(e.target.value)} autoFocus /> setAdminEmail(e.target.value)} /> setAdminPassword(e.target.value)} /> setAdminConfirm(e.target.value)} />
)} {/* ── Step 3: Branding ─────────────────────────────── */} {step === 3 && (

Branding & notifications

Customise the look of your site. You can change this later.

{logoPreview && ( Preview )} {suggestedColors.length > 0 && (

Suggested from your logo — click a swatch to apply it

{suggestedColors.map((hex) => (
))}
)} setNotifEmail(e.target.value)} />
)} {/* ── Step 4: Legal ─────────────────────────────────── */} {step === 4 && (

Legal pages

These values populate your Terms of Use and Privacy Policy. All fields are optional — you can fill them in later under Admin → Site Settings.

setLegalOperatorName(e.target.value)} /> setLegalWebsiteUrl(e.target.value)} /> setLegalEffectiveDate(e.target.value)} />

Information Officer (POPIA)

setLegalIoName(e.target.value)} /> setLegalIoEmail(e.target.value)} />
)} {/* ── Step 5: Email / SMTP (skippable) ─────────────── */} {step === 5 && (

Email delivery

Configure outgoing email so tickets, payment confirmations, and account notifications get delivered. You can skip this and configure it later under Admin → Site Settings (where you can also test the connection).

setSmtpHost(e.target.value)} autoFocus /> setSmtpPort(e.target.value)} />
setSmtpSecure(e.target.checked)} className="rounded" />
setSmtpFrom(e.target.value)} />
setSmtpUser(e.target.value)} autoComplete="username" /> setSmtpPass(e.target.value)} autoComplete="new-password" />
{smtpHost.trim() && (
{smtpTestResult && ( {smtpTestResult.ok ? "✓ " : "✗ "}{smtpTestResult.message} )}
)}
)} {/* ── Step 6: Done ─────────────────────────────────── */} {step === 6 && (

You're all set!

{orgName || "Your organisation"}'s events platform is ready. Log in with the admin account you just created.

{!smtpHost && (

Email delivery is not configured yet. Set it up under Admin → Site Settings after logging in.

)}
)} {step < 6 && (

Step {step} of {TOTAL_STEPS}

)}
); }