Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar shell, per-page help guides, and a layout/content pass across every remaining page) plus backend fixes to the dashboard KPI stats: - Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) now use a rolling trailing-month window (today back one calendar month, e.g. 9 May - 8 June if today is 8 June) instead of calendar month-to-date, which under-counted for most of the month. The comparison window shifts the same way, so like is still compared with like. - Reports deep-links from those stat tiles now match the same window (range=trailing_month, replacing range=this_month). - Design tokens (brand-* Tailwind scale + shadcn CSS variables), a site-wide contextual help button, fixed dashboard sidebar/navbar, Admin/Supervisor/Staff/User dashboard rebuilds backed by a new GET /api/stats/overview endpoint, a dedicated Contact page, Site Settings restyle with WhatsApp config folded in, and an Account activity feed backed by a new SecurityEvent model. - Every remaining page (home, events, registration flow, auth, legal, payment results, and every Admin/Supervisor/Staff/User tool page) restyled onto the same design tokens, several with real layout upgrades (home hero, events list/detail, donate page, auth pages). - 20+ new dedicated help guides so the whole site has page-specific help content instead of falling back to a generic guide. - Assorted fixes surfaced along the way: donation-leg double-counting in payment stats, donations not counting toward revenue, refund netting in per-method report breakdowns, and donation over-allocation after a refund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
531 lines
24 KiB
TypeScript
531 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { apiFetch, API_BASE } from "@/lib/api";
|
|
|
|
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 (
|
|
<div className="flex items-center gap-1 mb-8">
|
|
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map((s) => (
|
|
<React.Fragment key={s}>
|
|
<div
|
|
className={`h-2 flex-1 rounded-full transition-colors ${
|
|
s < step ? "bg-brand-600" : s === step ? "bg-brand-400" : "bg-gray-200"
|
|
}`}
|
|
/>
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Field({
|
|
label, required, hint, children,
|
|
}: {
|
|
label: string; required?: boolean; hint?: string; children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{label} {required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
{children}
|
|
{hint && <p className="text-xs text-gray-400 mt-1">{hint}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<Step>(1);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [setupToken, setSetupToken] = useState<string | null>(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 [accentColor, setAccentColor] = useState("#2563eb");
|
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
|
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 = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
setLogoFile(file);
|
|
setLogoPreview(URL.createObjectURL(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 {
|
|
let logoUrl = "";
|
|
if (logoFile) {
|
|
const fd = new FormData();
|
|
fd.append("image", logoFile);
|
|
const uploadRes = await fetch(`${API_BASE}/api/uploads/logo`, { method: "POST", body: fd });
|
|
if (uploadRes.ok) {
|
|
const up = await uploadRes.json();
|
|
logoUrl = up.url || "";
|
|
}
|
|
}
|
|
|
|
const settings: Record<string, string> = {
|
|
org_name: orgName.trim(),
|
|
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;
|
|
// 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 (
|
|
<div className="min-h-screen bg-gradient-to-br from-brand-50 to-brand-100 flex items-center justify-center p-4">
|
|
<div className="bg-white rounded-2xl shadow-lg w-full max-w-lg p-8">
|
|
|
|
{/* Heading */}
|
|
<div className="text-center mb-6">
|
|
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-brand-100 mb-3">
|
|
<svg className="w-7 h-7 text-brand-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Welcome</h1>
|
|
<p className="text-sm text-gray-500 mt-1">Let's set up your events platform</p>
|
|
</div>
|
|
|
|
{step < 6 && <ProgressBar step={step} />}
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 1: Organisation ──────────────────────────── */}
|
|
{step === 1 && (
|
|
<form onSubmit={handleStep1} className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Organisation details</h2>
|
|
<p className="text-sm text-gray-500">These appear throughout the site and in emails.</p>
|
|
</div>
|
|
|
|
<Field label="Organisation name" required>
|
|
<input className={inputCls} placeholder="e.g. Hope Family Church"
|
|
value={orgName} onChange={e => setOrgName(e.target.value)} autoFocus />
|
|
</Field>
|
|
<Field label="Tagline">
|
|
<input className={inputCls} placeholder="e.g. Connecting community through events"
|
|
value={orgTagline} onChange={e => setOrgTagline(e.target.value)} />
|
|
</Field>
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<Field label="Contact email">
|
|
<input type="email" className={inputCls} placeholder="admin@yourchurch.org"
|
|
value={orgEmail} onChange={e => setOrgEmail(e.target.value)} />
|
|
</Field>
|
|
<Field label="Phone number">
|
|
<input className={inputCls} placeholder="+27 12 345 6789"
|
|
value={orgPhone} onChange={e => setOrgPhone(e.target.value)} />
|
|
</Field>
|
|
</div>
|
|
<Field label="Address">
|
|
<input className={inputCls} placeholder="123 Church St, City"
|
|
value={orgAddress} onChange={e => setOrgAddress(e.target.value)} />
|
|
</Field>
|
|
<Field label="Site URL" hint="The public URL of this site — used in email links. e.g. https://events.yourchurch.org">
|
|
<input className={inputCls} placeholder="https://events.yourchurch.org"
|
|
value={appBaseUrl} onChange={e => setAppBaseUrl(e.target.value)} />
|
|
</Field>
|
|
|
|
<div className="pt-2 flex justify-end">
|
|
<button type="submit" className="px-5 py-2 bg-brand-600 hover:bg-brand-700 text-white rounded-lg text-sm font-medium">
|
|
Next →
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* ── Step 2: Admin account ─────────────────────────── */}
|
|
{step === 2 && (
|
|
<form onSubmit={handleStep2} className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Create admin account</h2>
|
|
<p className="text-sm text-gray-500">Your primary administrator login.</p>
|
|
</div>
|
|
|
|
<Field label="Full name" required>
|
|
<input className={inputCls} placeholder="Your name"
|
|
value={adminName} onChange={e => setAdminName(e.target.value)} autoFocus />
|
|
</Field>
|
|
<Field label="Email address" required>
|
|
<input type="email" className={inputCls} placeholder="admin@yourchurch.org"
|
|
value={adminEmail} onChange={e => setAdminEmail(e.target.value)} />
|
|
</Field>
|
|
<Field label="Password" required>
|
|
<input type="password" className={inputCls} placeholder="Minimum 8 characters"
|
|
value={adminPassword} onChange={e => setAdminPassword(e.target.value)} />
|
|
</Field>
|
|
<Field label="Confirm password" required>
|
|
<input type="password" className={inputCls} placeholder="Repeat password"
|
|
value={adminConfirm} onChange={e => setAdminConfirm(e.target.value)} />
|
|
</Field>
|
|
|
|
<div className="pt-2 flex justify-between">
|
|
<button type="button" onClick={back} className="px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-100">← Back</button>
|
|
<button type="submit" disabled={loading} className="px-5 py-2 bg-brand-600 hover:bg-brand-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium">
|
|
{loading ? "Creating account…" : "Next →"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* ── Step 3: Branding ─────────────────────────────── */}
|
|
{step === 3 && (
|
|
<form onSubmit={handleStep3} className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Branding & notifications</h2>
|
|
<p className="text-sm text-gray-500">Customise the look of your site. You can change this later.</p>
|
|
</div>
|
|
|
|
<Field label="Accent / brand colour">
|
|
<div className="flex items-center gap-3">
|
|
<input type="color" className="h-10 w-20 border rounded cursor-pointer"
|
|
value={accentColor} onChange={e => setAccentColor(e.target.value)} />
|
|
<input className={`${inputCls} font-mono`} value={accentColor}
|
|
onChange={e => setAccentColor(e.target.value)} placeholder="#2563eb" />
|
|
</div>
|
|
<div className="mt-2 h-8 rounded-lg" style={{ background: accentColor }} />
|
|
</Field>
|
|
|
|
<Field label="Logo image (optional)" hint="PNG, JPG, SVG or WebP, max 2 MB. If skipped, a default logo is used.">
|
|
<input type="file" accept="image/*" onChange={handleLogoChange} className="text-sm" />
|
|
{logoPreview && (
|
|
<img src={logoPreview} alt="Preview" className="mt-2 h-16 object-contain rounded border" />
|
|
)}
|
|
</Field>
|
|
|
|
<Field label="Registration notification email"
|
|
hint="Who to notify when someone registers for an event. Separate multiple addresses with commas.">
|
|
<input type="email" className={inputCls} placeholder="registrations@yourchurch.org"
|
|
value={notifEmail} onChange={e => setNotifEmail(e.target.value)} />
|
|
</Field>
|
|
|
|
<div className="pt-2 flex justify-between">
|
|
<button type="button" onClick={back} className="px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-100">← Back</button>
|
|
<button type="submit" className="px-5 py-2 bg-brand-600 hover:bg-brand-700 text-white rounded-lg text-sm font-medium">Next →</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* ── Step 4: Legal ─────────────────────────────────── */}
|
|
{step === 4 && (
|
|
<form onSubmit={handleStep4} className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Legal pages</h2>
|
|
<p className="text-sm text-gray-500">
|
|
These values populate your Terms of Use and Privacy Policy. All fields are optional
|
|
— you can fill them in later under Admin → Site Settings.
|
|
</p>
|
|
</div>
|
|
|
|
<Field label="Operator / responsible party"
|
|
hint='Shown in "Owned and operated by" on the Terms of Use page.'>
|
|
<input className={inputCls}
|
|
placeholder="Jane Smith on behalf of Example Church, City, South Africa"
|
|
value={legalOperatorName} onChange={e => setLegalOperatorName(e.target.value)} />
|
|
</Field>
|
|
<Field label="Website URL (without https://)"
|
|
hint="e.g. events.yourchurch.org">
|
|
<input className={inputCls} placeholder="events.yourchurch.org"
|
|
value={legalWebsiteUrl} onChange={e => setLegalWebsiteUrl(e.target.value)} />
|
|
</Field>
|
|
<Field label="Effective date" hint='e.g. "April 2026" or "1 April 2026"'>
|
|
<input className={inputCls} placeholder="April 2026"
|
|
value={legalEffectiveDate} onChange={e => setLegalEffectiveDate(e.target.value)} />
|
|
</Field>
|
|
|
|
<div className="border-t pt-4">
|
|
<p className="text-sm font-medium text-gray-700 mb-3">Information Officer (POPIA)</p>
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<Field label="Full name">
|
|
<input className={inputCls} placeholder="Jane Smith"
|
|
value={legalIoName} onChange={e => setLegalIoName(e.target.value)} />
|
|
</Field>
|
|
<Field label="Email">
|
|
<input type="email" className={inputCls} placeholder="io@yourchurch.org"
|
|
value={legalIoEmail} onChange={e => setLegalIoEmail(e.target.value)} />
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-2 flex justify-between">
|
|
<button type="button" onClick={back} className="px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-100">← Back</button>
|
|
<button type="submit" className="px-5 py-2 bg-brand-600 hover:bg-brand-700 text-white rounded-lg text-sm font-medium">Next →</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* ── Step 5: Email / SMTP (skippable) ─────────────── */}
|
|
{step === 5 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Email delivery</h2>
|
|
<p className="text-sm text-gray-500">
|
|
Configure outgoing email so tickets, payment confirmations, and account
|
|
notifications get delivered. <span className="font-medium text-gray-700">You can skip this and configure it later</span> under
|
|
Admin → Site Settings (where you can also test the connection).
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<Field label="SMTP host">
|
|
<input className={inputCls} placeholder="smtp.yourprovider.com"
|
|
value={smtpHost} onChange={e => setSmtpHost(e.target.value)} autoFocus />
|
|
</Field>
|
|
<Field label="Port">
|
|
<input type="number" className={inputCls} placeholder="587"
|
|
value={smtpPort} onChange={e => setSmtpPort(e.target.value)} />
|
|
</Field>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" id="smtpSecure" checked={smtpSecure}
|
|
onChange={e => setSmtpSecure(e.target.checked)} className="rounded" />
|
|
<label htmlFor="smtpSecure" className="text-sm text-gray-700">Use TLS/SSL (port 465)</label>
|
|
</div>
|
|
|
|
<Field label="From address" hint="The address emails appear to come from.">
|
|
<input type="email" className={inputCls} placeholder="no-reply@yourchurch.org"
|
|
value={smtpFrom} onChange={e => setSmtpFrom(e.target.value)} />
|
|
</Field>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<Field label="SMTP username">
|
|
<input type="email" className={inputCls} placeholder="mail@yourchurch.org"
|
|
value={smtpUser} onChange={e => setSmtpUser(e.target.value)} autoComplete="username" />
|
|
</Field>
|
|
<Field label="SMTP password" hint="Stored encrypted.">
|
|
<input type="password" className={inputCls} placeholder="Password"
|
|
value={smtpPass} onChange={e => setSmtpPass(e.target.value)} autoComplete="new-password" />
|
|
</Field>
|
|
</div>
|
|
|
|
{smtpHost.trim() && (
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
disabled={smtpTesting}
|
|
onClick={handleTestSmtp}
|
|
className="px-4 py-2 rounded-lg text-sm border border-gray-300 text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
|
>
|
|
{smtpTesting ? "Testing…" : "Test connection"}
|
|
</button>
|
|
{smtpTestResult && (
|
|
<span className={`text-sm ${smtpTestResult.ok ? "text-green-600" : "text-red-600"}`}>
|
|
{smtpTestResult.ok ? "✓ " : "✗ "}{smtpTestResult.message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-2 flex justify-between items-center flex-wrap gap-2">
|
|
<button type="button" onClick={back} className="px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-100">← Back</button>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
disabled={loading}
|
|
onClick={() => handleFinish(true)}
|
|
className="px-4 py-2 rounded-lg text-sm text-gray-500 hover:bg-gray-100 disabled:opacity-50"
|
|
>
|
|
Skip for now
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={loading}
|
|
onClick={() => handleFinish(false)}
|
|
className="px-5 py-2 bg-brand-600 hover:bg-brand-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium"
|
|
>
|
|
{loading ? "Setting up…" : "Finish setup"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 6: Done ─────────────────────────────────── */}
|
|
{step === 6 && (
|
|
<div className="text-center space-y-4">
|
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-2">
|
|
<svg className="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-bold text-gray-900">You're all set!</h2>
|
|
<p className="text-sm text-gray-500">
|
|
{orgName || "Your organisation"}'s events platform is ready. Log in with the admin account you just created.
|
|
</p>
|
|
{!smtpHost && (
|
|
<p className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
|
Email delivery is not configured yet. Set it up under Admin → Site Settings after logging in.
|
|
</p>
|
|
)}
|
|
<button
|
|
onClick={() => router.push("/login")}
|
|
className="mt-4 px-6 py-2.5 bg-brand-600 hover:bg-brand-700 text-white rounded-lg text-sm font-medium"
|
|
>
|
|
Go to login
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{step < 6 && (
|
|
<p className="text-center text-xs text-gray-400 mt-6">Step {step} of {TOTAL_STEPS}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |