insanely simple onboarding: 1-screen signup → dashboard checklist

OLD FLOW (8+ screens):
  signup (4 fields) → auto-login → setup wizard step 1 → step 2 → step 3 → step 4 → dashboard

NEW FLOW (2 screens):
  signup (3 fields) → dashboard with inline checklist

- Signup page: just charity name + email + password. No 'your name' field. One button.
- Dashboard: shows getting-started checklist when org has no pledges yet
- /api/onboarding: returns setup progress (bank, event, qr, pledge)
- Checklist: progress bar, next-step highlighting, done states with strikethrough
- Each step links directly to the right page (settings, events, pledges)
- Tip shown for brand new orgs: 'Add bank details first'
- No more separate setup wizard — guidance is inline on the dashboard
- Signup loading state: pulsing emoji while account creates
This commit is contained in:
2026-03-03 06:05:10 +08:00
parent 12ea9691c4
commit 369860d8b9
3 changed files with 156 additions and 99 deletions

View File

@@ -6,137 +6,115 @@ import { useRouter } from "next/navigation"
import Link from "next/link" import Link from "next/link"
export default function SignupPage() { export default function SignupPage() {
const [step, setStep] = useState<"form" | "loading">("form")
const [charityName, setCharityName] = useState("") const [charityName, setCharityName] = useState("")
const [name, setName] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const router = useRouter() const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!charityName.trim() || !email.trim() || !password) return
setError("") setError("")
setLoading(true) setStep("loading")
try { try {
const res = await fetch("/api/auth/signup", { const res = await fetch("/api/auth/signup", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, name, charityName }), body: JSON.stringify({ email, password, charityName, name: "" }),
}) })
const data = await res.json() const data = await res.json()
if (!res.ok) { if (!res.ok) {
setError(data.error || "Failed to create account") setError(data.error || "Something went wrong")
setLoading(false) setStep("form")
return return
} }
// Auto sign in // Auto sign in and go straight to dashboard
const result = await signIn("credentials", { const result = await signIn("credentials", { email, password, redirect: false })
email,
password,
redirect: false,
})
if (result?.error) { if (result?.error) {
setError("Account created but couldn't sign in. Try logging in.") setError("Account created — please sign in")
setLoading(false) setStep("form")
} else { } else {
router.push("/dashboard/setup") router.push("/dashboard")
} }
} catch { } catch {
setError("Something went wrong. Please try again.") setError("Connection error. Try again.")
setLoading(false) setStep("form")
} }
} }
if (step === "loading") {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5">
<div className="text-center space-y-4">
<div className="inline-flex h-14 w-14 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 items-center justify-center shadow-lg shadow-trust-blue/20 animate-pulse">
<span className="text-white text-2xl">🤲</span>
</div>
<p className="text-sm font-medium text-trust-blue animate-pulse">Setting up your charity...</p>
</div>
</div>
)
}
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 p-4">
<div className="w-full max-w-sm space-y-6"> <div className="w-full max-w-sm space-y-5">
<div className="text-center"> <div className="text-center">
<div className="inline-flex h-12 w-12 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 items-center justify-center shadow-lg shadow-trust-blue/20 mb-4"> <div className="inline-flex h-12 w-12 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 items-center justify-center shadow-lg shadow-trust-blue/20 mb-3">
<span className="text-white text-xl">🤲</span> <span className="text-white text-xl">🤲</span>
</div> </div>
<h1 className="text-2xl font-black text-gray-900">Get Started Free</h1> <h1 className="text-2xl font-black text-gray-900">Start collecting pledges</h1>
<p className="text-sm text-muted-foreground mt-1">Set up your charity in 2 minutes</p> <p className="text-sm text-muted-foreground mt-1">Free. 30 seconds. No card.</p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-3">
{error && ( {error && (
<div className="rounded-xl bg-danger-red/10 border border-danger-red/20 p-3 text-sm text-danger-red text-center"> <div className="rounded-xl bg-danger-red/10 border border-danger-red/20 p-2.5 text-sm text-danger-red text-center">{error}</div>
{error}
</div>
)} )}
<div> <input
<label className="text-sm font-medium text-gray-700">Charity / Organisation Name</label> type="text"
<input value={charityName}
type="text" onChange={(e) => setCharityName(e.target.value)}
value={charityName} className="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all"
onChange={(e) => setCharityName(e.target.value)} placeholder="Your charity or mosque name"
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" required
placeholder="e.g. Islamic Relief UK" autoFocus
required />
/>
</div>
<div> <input
<label className="text-sm font-medium text-gray-700">Your Name</label> type="email"
<input value={email}
type="text" onChange={(e) => setEmail(e.target.value)}
value={name} className="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all"
onChange={(e) => setName(e.target.value)} placeholder="Your email"
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" required
placeholder="e.g. Fatima Khan" />
/>
</div>
<div> <input
<label className="text-sm font-medium text-gray-700">Email</label> type="password"
<input value={password}
type="email" onChange={(e) => setPassword(e.target.value)}
value={email} className="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all"
onChange={(e) => setEmail(e.target.value)} placeholder="Pick a password (8+ chars)"
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all" required
placeholder="you@charity.org" minLength={8}
required />
/>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-3 text-sm focus:border-trust-blue focus:ring-2 focus:ring-trust-blue/20 outline-none transition-all"
placeholder="Min 8 characters"
required
minLength={8}
/>
</div>
<button <button
type="submit" type="submit"
disabled={loading} className="w-full rounded-xl bg-trust-blue px-4 py-3.5 text-sm font-bold text-white hover:bg-trust-blue/90 transition-all shadow-lg shadow-trust-blue/20"
className="w-full rounded-xl bg-trust-blue px-4 py-3 text-sm font-semibold text-white hover:bg-trust-blue/90 disabled:opacity-50 transition-all"
> >
{loading ? "Creating your account..." : "Create Account & Set Up →"} Create Account
</button> </button>
<p className="text-[10px] text-center text-muted-foreground">
Free forever. No credit card needed. Takes 2 minutes.
</p>
</form> </form>
<p className="text-center text-sm text-muted-foreground"> <p className="text-center text-xs text-muted-foreground">
Already have an account?{" "} Already have an account? <Link href="/login" className="text-trust-blue font-semibold hover:underline">Sign in</Link>
<Link href="/login" className="text-trust-blue font-semibold hover:underline">
Sign In
</Link>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,38 @@
import { NextResponse } from "next/server"
import prisma from "@/lib/prisma"
import { getOrgId } from "@/lib/session"
/**
* GET /api/onboarding — check setup progress for current org
*/
export async function GET() {
const orgId = await getOrgId(null)
if (!orgId || !prisma) return NextResponse.json({ steps: [] })
const [org, eventCount, qrCount, pledgeCount] = await Promise.all([
prisma.organization.findUnique({
where: { id: orgId },
select: { name: true, bankSortCode: true, bankAccountNo: true, bankAccountName: true },
}),
prisma.event.count({ where: { organizationId: orgId } }),
prisma.qrSource.count({ where: { event: { organizationId: orgId } } }),
prisma.pledge.count({ where: { organizationId: orgId } }),
])
const hasBank = !!(org?.bankSortCode && org?.bankAccountNo)
const hasEvent = eventCount > 0
const hasQr = qrCount > 0
const hasPledge = pledgeCount > 0
const steps = [
{ id: "bank", label: "Add bank details", desc: "So donors know where to send money", done: hasBank, href: "/dashboard/settings" },
{ id: "event", label: "Create an event", desc: "Give your fundraiser a name", done: hasEvent, href: "/dashboard/events" },
{ id: "qr", label: "Generate a QR code", desc: "One per table or volunteer", done: hasQr, href: hasEvent ? "/dashboard/events" : "/dashboard/events" },
{ id: "pledge", label: "Get your first pledge", desc: "Share the link or scan the QR", done: hasPledge, href: "/dashboard/pledges" },
]
const completed = steps.filter(s => s.done).length
const allDone = completed === steps.length
return NextResponse.json({ steps, completed, total: steps.length, allDone, orgName: org?.name })
}

View File

@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress"
import { formatPence } from "@/lib/utils" import { formatPence } from "@/lib/utils"
import { TrendingUp, Users, Banknote, AlertTriangle, Calendar, Clock, CheckCircle2, ArrowRight, Loader2, MessageCircle, ExternalLink } from "lucide-react" import { TrendingUp, Users, Banknote, AlertTriangle, Calendar, Clock, CheckCircle2, ArrowRight, Loader2, MessageCircle, ExternalLink, Circle } from "lucide-react"
import Link from "next/link" import Link from "next/link"
interface DashboardData { interface DashboardData {
@@ -31,6 +31,7 @@ export default function DashboardPage() {
const [data, setData] = useState<DashboardData | null>(null) const [data, setData] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [whatsappStatus, setWhatsappStatus] = useState<boolean | null>(null) const [whatsappStatus, setWhatsappStatus] = useState<boolean | null>(null)
const [onboarding, setOnboarding] = useState<{ steps: Array<{ id: string; label: string; desc: string; done: boolean; href: string }>; completed: number; total: number; allDone: boolean } | null>(null)
const fetchData = useCallback(() => { const fetchData = useCallback(() => {
fetch("/api/dashboard") fetch("/api/dashboard")
@@ -43,6 +44,7 @@ export default function DashboardPage() {
useEffect(() => { useEffect(() => {
fetchData() fetchData()
fetch("/api/whatsapp/send").then(r => r.json()).then(d => setWhatsappStatus(d.connected)).catch(() => {}) fetch("/api/whatsapp/send").then(r => r.json()).then(d => setWhatsappStatus(d.connected)).catch(() => {})
fetch("/api/onboarding").then(r => r.json()).then(d => { if (d.steps) setOnboarding(d) }).catch(() => {})
const interval = setInterval(fetchData, 15000) const interval = setInterval(fetchData, 15000)
return () => clearInterval(interval) return () => clearInterval(interval)
}, [fetchData]) }, [fetchData])
@@ -55,22 +57,61 @@ export default function DashboardPage() {
) )
} }
if (!data) { if (!data || (data.summary.totalPledges === 0 && onboarding && !onboarding.allDone)) {
// Show getting-started checklist
const ob = onboarding
return ( return (
<div className="text-center py-20 space-y-4"> <div className="max-w-lg mx-auto space-y-6 py-4">
<Calendar className="h-12 w-12 text-muted-foreground mx-auto" /> <div className="text-center space-y-2">
<h2 className="text-xl font-bold">Welcome to Pledge Now, Pay Later</h2> <div className="inline-flex h-14 w-14 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 items-center justify-center shadow-lg shadow-trust-blue/20">
<p className="text-muted-foreground max-w-md mx-auto"> <span className="text-white text-2xl">🤲</span>
Start by configuring your organisation&apos;s bank details, then create your first event. </div>
</p> <h1 className="text-2xl font-black text-gray-900">Let&apos;s get you set up</h1>
<div className="flex gap-3 justify-center"> <p className="text-sm text-muted-foreground">4 quick steps, then you&apos;re collecting pledges</p>
<Link href="/dashboard/settings">
<Button variant="outline">Configure Bank Details</Button>
</Link>
<Link href="/dashboard/events">
<Button>Create First Event </Button>
</Link>
</div> </div>
{ob && (
<>
<Progress value={(ob.completed / ob.total) * 100} className="h-2" indicatorClassName="bg-gradient-to-r from-trust-blue to-success-green" />
<p className="text-xs text-center text-muted-foreground">{ob.completed} of {ob.total} done</p>
<div className="space-y-2">
{ob.steps.map((step, i) => {
const isNext = !step.done && ob.steps.slice(0, i).every(s => s.done)
return (
<Link key={step.id} href={step.href}>
<div className={`flex items-center gap-3 rounded-xl border p-4 transition-all ${
step.done ? "bg-success-green/5 border-success-green/20" :
isNext ? "bg-trust-blue/5 border-trust-blue/20 shadow-sm" :
"bg-white border-gray-100"
}`}>
{step.done ? (
<CheckCircle2 className="h-5 w-5 text-success-green flex-shrink-0" />
) : isNext ? (
<div className="h-5 w-5 rounded-full bg-trust-blue text-white text-xs font-bold flex items-center justify-center flex-shrink-0">{i + 1}</div>
) : (
<Circle className="h-5 w-5 text-gray-300 flex-shrink-0" />
)}
<div className="flex-1">
<p className={`text-sm font-medium ${step.done ? "text-success-green line-through" : isNext ? "text-gray-900" : "text-gray-400"}`}>{step.label}</p>
<p className="text-xs text-muted-foreground">{step.desc}</p>
</div>
{isNext && <ArrowRight className="h-4 w-4 text-trust-blue flex-shrink-0" />}
</div>
</Link>
)
})}
</div>
</>
)}
{(!ob || ob.completed === 0) && (
<div className="bg-warm-amber/5 rounded-xl border border-warm-amber/20 p-4 text-center">
<p className="text-xs text-muted-foreground">
💡 <strong>Tip:</strong> Add your bank details first that&apos;s the only thing you need before donors can pledge.
</p>
</div>
)}
</div> </div>
) )
} }