feat: premium UI overhaul, AI suggestions, WAHA WhatsApp integration
PREMIUM UI: - All animations: fade-up, scale-in, stagger children, confetti celebration - Glass effects, gradient icons, premium card hover states - Custom CSS: shimmer, pulse-ring, bounce, counter-roll animations - Smooth progress bar with gradient AI-POWERED (GPT-4o-mini nano model): - Smart amount suggestions based on peer data (/api/ai/suggest) - Social proof: '42 people pledged · Average £85' - AI-generated nudge text for conversion - AI fuzzy matching for bank reconciliation - AI reminder message generation WAHA WHATSAPP INTEGRATION: - Auto-send pledge receipt with bank details via WhatsApp - 4-step reminder sequence: gentle → nudge → urgent → final - Chatbot: donors reply PAID, HELP, CANCEL, STATUS - Volunteer notification on new pledges - WhatsApp status in dashboard settings - Webhook endpoint for incoming messages DONOR FLOW (CRO): - Amount step: AI suggestions, Gift Aid preview, social proof, haptic feedback - Payment step: trust signals, fee comparison, benefit badges - Identity step: email/phone toggle, WhatsApp reminder indicator - Bank instructions: tap-to-copy each field, WhatsApp delivery confirmation - Confirmation: confetti, pulse animation, share CTA, WhatsApp receipt COMPOSE: - Added WAHA env vars + qc-comms network for WhatsApp access
This commit is contained in:
@@ -29,15 +29,6 @@ interface EventInfo {
|
||||
qrSourceLabel: string | null
|
||||
}
|
||||
|
||||
// Steps:
|
||||
// 0 = Amount
|
||||
// 1 = Payment method
|
||||
// 2 = Identity (for bank transfer)
|
||||
// 3 = Bank instructions
|
||||
// 4 = Confirmation (card, DD)
|
||||
// 5 = Card payment step
|
||||
// 7 = Direct Debit step
|
||||
|
||||
export default function PledgePage() {
|
||||
const params = useParams()
|
||||
const token = params.token as string
|
||||
@@ -54,12 +45,7 @@ export default function PledgePage() {
|
||||
const [pledgeResult, setPledgeResult] = useState<{
|
||||
id: string
|
||||
reference: string
|
||||
bankDetails?: {
|
||||
bankName: string
|
||||
sortCode: string
|
||||
accountNo: string
|
||||
accountName: string
|
||||
}
|
||||
bankDetails?: { bankName: string; sortCode: string; accountNo: string; accountName: string }
|
||||
} | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState("")
|
||||
@@ -72,10 +58,7 @@ export default function PledgePage() {
|
||||
else setEventInfo(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Unable to load pledge page")
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => { setError("Unable to load pledge page"); setLoading(false) })
|
||||
fetch("/api/analytics", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -86,16 +69,21 @@ export default function PledgePage() {
|
||||
const handleAmountSelected = (amountPence: number) => {
|
||||
setPledgeData((d) => ({ ...d, amountPence }))
|
||||
setStep(1)
|
||||
fetch("/api/analytics", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ eventType: "amount_selected", metadata: { amountPence, token } }),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleRailSelected = (rail: Rail) => {
|
||||
setPledgeData((d) => ({ ...d, rail }))
|
||||
const railStepMap: Record<Rail, number> = {
|
||||
bank: 2,
|
||||
card: 5,
|
||||
gocardless: 7,
|
||||
}
|
||||
setStep(railStepMap[rail])
|
||||
fetch("/api/analytics", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ eventType: "rail_selected", metadata: { rail, token } }),
|
||||
}).catch(() => {})
|
||||
setStep(rail === "bank" ? 2 : rail === "card" ? 5 : 7)
|
||||
}
|
||||
|
||||
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean }) => {
|
||||
@@ -106,11 +94,7 @@ export default function PledgePage() {
|
||||
const res = await fetch("/api/pledges", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...finalData,
|
||||
eventId: eventInfo?.id,
|
||||
qrSourceId: eventInfo?.qrSourceId,
|
||||
}),
|
||||
body: JSON.stringify({ ...finalData, eventId: eventInfo?.id, qrSourceId: eventInfo?.qrSourceId }),
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) { setError(result.error); return }
|
||||
@@ -123,8 +107,11 @@ export default function PledgePage() {
|
||||
|
||||
if (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="animate-pulse text-trust-blue text-lg font-medium">Loading...</div>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 flex items-center justify-center animate-pulse">
|
||||
<span className="text-white text-xl">🤲</span>
|
||||
</div>
|
||||
<p className="text-trust-blue font-medium animate-pulse">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -132,49 +119,61 @@ export default function PledgePage() {
|
||||
if (error) {
|
||||
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="text-center space-y-4">
|
||||
<div className="text-6xl">😔</div>
|
||||
<div className="text-center space-y-4 animate-fade-up">
|
||||
<div className="text-5xl">😔</div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Something went wrong</h1>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
<button onClick={() => window.location.reload()} className="text-trust-blue font-medium hover:underline">
|
||||
Try again →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const shareUrl = eventInfo?.qrSourceId ? `${typeof window !== "undefined" ? window.location.origin : ""}/p/${token}` : undefined
|
||||
const shareUrl = `${typeof window !== "undefined" ? window.location.origin : ""}/p/${token}`
|
||||
|
||||
const steps: Record<number, React.ReactNode> = {
|
||||
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} />,
|
||||
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
|
||||
1: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
|
||||
2: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} />,
|
||||
3: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} />,
|
||||
4: pledgeResult && <ConfirmationStep pledge={pledgeResult} amount={pledgeData.amountPence} rail={pledgeData.rail} eventName={eventInfo?.name || ""} shareUrl={shareUrl} />,
|
||||
3: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
|
||||
4: pledgeResult && <ConfirmationStep pledge={pledgeResult} amount={pledgeData.amountPence} rail={pledgeData.rail} eventName={eventInfo?.name || ""} shareUrl={shareUrl} donorPhone={pledgeData.donorPhone} />,
|
||||
5: <CardPaymentStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} eventId={eventInfo?.id || ""} qrSourceId={eventInfo?.qrSourceId || null} onComplete={submitPledge} />,
|
||||
7: <DirectDebitStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} organizationName={eventInfo?.organizationName || ""} eventId={eventInfo?.id || ""} qrSourceId={eventInfo?.qrSourceId || null} onComplete={submitPledge} />,
|
||||
}
|
||||
|
||||
const backableSteps = new Set([1, 2, 5, 7])
|
||||
const getBackStep = (s: number): number => {
|
||||
if (s === 5 || s === 7) return 1
|
||||
return s - 1
|
||||
}
|
||||
const progressPercent = step >= 3 ? 100 : step >= 2 ? 66 : step >= 1 ? 33 : 10
|
||||
const getBackStep = (s: number): number => (s === 5 || s === 7) ? 1 : s - 1
|
||||
|
||||
// Smooth progress
|
||||
const progressMap: Record<number, number> = { 0: 8, 1: 33, 2: 55, 3: 100, 4: 100, 5: 55, 7: 55 }
|
||||
const progressPercent = progressMap[step] || 10
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5">
|
||||
{/* Progress bar */}
|
||||
<div className="fixed top-0 left-0 right-0 h-1 bg-gray-100 z-50">
|
||||
<div className="h-full bg-trust-blue transition-all duration-500 ease-out" style={{ width: `${progressPercent}%` }} />
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-trust-blue to-success-green transition-all duration-700 ease-out"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 pb-2 px-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">{eventInfo?.organizationName}</p>
|
||||
<p className="text-xs text-muted-foreground/60">{eventInfo?.qrSourceLabel || ""}</p>
|
||||
{/* Header */}
|
||||
<div className="pt-6 pb-1 px-4 text-center">
|
||||
<p className="text-xs font-medium text-muted-foreground">{eventInfo?.organizationName}</p>
|
||||
{eventInfo?.qrSourceLabel && (
|
||||
<p className="text-[10px] text-muted-foreground/60">{eventInfo.qrSourceLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-8">{steps[step]}</div>
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-20">{steps[step]}</div>
|
||||
|
||||
{/* Back button */}
|
||||
{backableSteps.has(step) && (
|
||||
<div className="fixed bottom-6 left-4">
|
||||
<div className="fixed bottom-0 left-0 right-0 pb-6 pt-4 px-4 bg-gradient-to-t from-white via-white/80 to-transparent">
|
||||
<button
|
||||
onClick={() => setStep(getBackStep(step))}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors tap-target flex items-center gap-1"
|
||||
|
||||
Reference in New Issue
Block a user