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:
2026-03-03 04:31:07 +08:00
parent 0236867c88
commit c6e7e4f01e
15 changed files with 1473 additions and 383 deletions

View File

@@ -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"

View File

@@ -1,23 +1,48 @@
"use client"
import { useState } from "react"
import { useState, useEffect, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
const PRESETS = [1000, 2000, 5000, 10000, 25000, 50000] // pence
import { Heart, Sparkles, TrendingUp } from "lucide-react"
interface Props {
onSelect: (amountPence: number) => void
eventName: string
eventId?: string
}
export function AmountStep({ onSelect, eventName }: Props) {
interface AiSuggestion {
amounts: number[]
nudge: string
socialProof: string
}
const FALLBACK_AMOUNTS = [2000, 5000, 10000, 25000, 50000, 100000]
export function AmountStep({ onSelect, eventName, eventId }: Props) {
const [custom, setCustom] = useState("")
const [selected, setSelected] = useState<number | null>(null)
const [suggestions, setSuggestions] = useState<AiSuggestion | null>(null)
const [hovering, setHovering] = useState<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
// Fetch AI-powered suggestions
useEffect(() => {
const url = eventId ? `/api/ai/suggest?eventId=${eventId}` : "/api/ai/suggest"
fetch(url)
.then(r => r.json())
.then(data => {
if (data.amounts?.length) setSuggestions(data)
})
.catch(() => {})
}, [eventId])
const amounts = suggestions?.amounts || FALLBACK_AMOUNTS
const handlePreset = (amount: number) => {
setSelected(amount)
setCustom("")
// Haptic feedback on mobile
if (navigator.vibrate) navigator.vibrate(10)
}
const handleCustomChange = (value: string) => {
@@ -31,66 +56,133 @@ export function AmountStep({ onSelect, eventName }: Props) {
if (amount >= 100) onSelect(amount)
}
const isValid = selected || (custom && parseFloat(custom) >= 1)
const activeAmount = selected || (custom ? Math.round(parseFloat(custom) * 100) : 0)
const isValid = activeAmount >= 100
const giftAidBonus = Math.round(activeAmount * 0.25)
return (
<div className="max-w-md mx-auto pt-4 space-y-8">
<div className="text-center space-y-2">
<h1 className="text-3xl font-extrabold text-gray-900">
<div className="max-w-md mx-auto pt-2 space-y-6 animate-fade-up">
{/* Hero */}
<div className="text-center space-y-3">
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 shadow-lg shadow-trust-blue/30">
<Heart className="h-7 w-7 text-white" />
</div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">
Make a Pledge
</h1>
<p className="text-muted-foreground">
<p className="text-base text-muted-foreground">
for <span className="font-semibold text-foreground">{eventName}</span>
</p>
</div>
{/* Presets */}
<div className="grid grid-cols-3 gap-3">
{PRESETS.map((amount) => (
<button
key={amount}
onClick={() => handlePreset(amount)}
className={`
tap-target rounded-2xl border-2 py-4 text-center font-bold text-lg transition-all
${selected === amount
? "border-trust-blue bg-trust-blue text-white shadow-lg shadow-trust-blue/25 scale-[1.02]"
: "border-gray-200 bg-white text-gray-900 hover:border-trust-blue/50 active:scale-[0.98]"
}
`}
>
£{amount / 100}
</button>
))}
{/* Social proof */}
{suggestions?.socialProof && (
<div className="flex items-center justify-center gap-2 animate-fade-in">
<div className="flex -space-x-2">
{[...Array(3)].map((_, i) => (
<div key={i} className="w-6 h-6 rounded-full bg-gradient-to-br from-trust-blue to-blue-400 border-2 border-white flex items-center justify-center">
<span className="text-[8px] text-white font-bold">{["A", "S", "M"][i]}</span>
</div>
))}
</div>
<p className="text-sm text-muted-foreground">
<TrendingUp className="h-3.5 w-3.5 inline mr-1 text-success-green" />
{suggestions.socialProof}
</p>
</div>
)}
{/* Amount grid */}
<div className="grid grid-cols-3 gap-2.5 stagger-children">
{amounts.map((amount) => {
const isSelected = selected === amount
const isHovering = hovering === amount
const pounds = amount / 100
return (
<button
key={amount}
onClick={() => handlePreset(amount)}
onMouseEnter={() => setHovering(amount)}
onMouseLeave={() => setHovering(null)}
className={`
relative tap-target rounded-2xl border-2 py-4 text-center font-bold transition-all duration-200
${isSelected
? "border-trust-blue bg-trust-blue text-white shadow-xl shadow-trust-blue/30 scale-[1.03]"
: isHovering
? "border-trust-blue/40 bg-trust-blue/5 text-gray-900"
: "border-gray-200 bg-white text-gray-900 hover:border-trust-blue/30"
}
`}
>
<span className="text-xl">£{pounds >= 1000 ? `${pounds / 1000}k` : pounds}</span>
{/* Gift Aid indicator */}
{isSelected && (
<div className="absolute -top-2 -right-2 bg-success-green text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full animate-scale-in shadow-sm">
+£{Math.round(amount * 0.25 / 100)}
</div>
)}
</button>
)
})}
</div>
{/* Custom */}
{/* AI nudge */}
{suggestions?.nudge && (
<p className="text-center text-sm text-muted-foreground/80 italic flex items-center justify-center gap-1.5 animate-fade-in" style={{ animationDelay: "400ms" }}>
<Sparkles className="h-3.5 w-3.5 text-warm-amber" />
{suggestions.nudge}
</p>
)}
{/* Custom amount — premium input */}
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">Or enter a custom amount</label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-2xl font-bold text-gray-400">£</span>
<Input
<button
onClick={() => inputRef.current?.focus()}
className="text-xs font-medium text-muted-foreground hover:text-trust-blue transition-colors cursor-pointer"
>
Or enter your own amount
</button>
<div className="relative group">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-2xl font-black text-gray-300 group-focus-within:text-trust-blue transition-colors">£</span>
<input
ref={inputRef}
type="text"
inputMode="decimal"
placeholder="0.00"
placeholder="0"
value={custom}
onChange={(e) => handleCustomChange(e.target.value)}
className="pl-10 h-16 text-2xl font-bold text-center rounded-2xl"
className="w-full pl-10 pr-4 h-16 text-2xl font-black text-center rounded-2xl border-2 border-gray-200 bg-white focus:border-trust-blue focus:ring-4 focus:ring-trust-blue/10 outline-none transition-all"
/>
</div>
</div>
{/* Live Gift Aid preview */}
{isValid && (
<div className="rounded-2xl bg-gradient-to-r from-success-green/5 to-success-green/10 border border-success-green/20 p-4 text-center animate-scale-in">
<p className="text-sm">
<span className="font-bold text-success-green">With Gift Aid:</span>{" "}
your £{(activeAmount / 100).toFixed(0)} becomes{" "}
<span className="font-black text-success-green text-lg">
£{((activeAmount + giftAidBonus) / 100).toFixed(0)}
</span>
</p>
<p className="text-xs text-muted-foreground mt-0.5">HMRC adds 25% at zero cost to you</p>
</div>
)}
{/* Continue */}
<Button
size="xl"
className="w-full"
className={`w-full transition-all duration-300 ${isValid ? "opacity-100 translate-y-0" : "opacity-50 translate-y-1"}`}
disabled={!isValid}
onClick={handleContinue}
>
Continue
{isValid ? `Pledge £${(activeAmount / 100).toFixed(activeAmount % 100 === 0 ? 0 : 2)}` : "Choose an amount"}
</Button>
<p className="text-center text-xs text-muted-foreground">
You won&apos;t be charged now. Choose how to pay next.
No payment now choose how to pay on the next step
</p>
</div>
)

View File

@@ -1,9 +1,9 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Check, Copy, ExternalLink, MessageCircle, Share2 } from "lucide-react"
import { Check, Copy, MessageCircle, Share2, Sparkles, ExternalLink } from "lucide-react"
interface Props {
pledge: {
@@ -18,26 +18,52 @@ interface Props {
}
amount: number
eventName: string
donorPhone?: string
}
export function BankInstructionsStep({ pledge, amount, eventName }: Props) {
const [copied, setCopied] = useState(false)
export function BankInstructionsStep({ pledge, amount, eventName, donorPhone }: Props) {
const [copiedField, setCopiedField] = useState<string | null>(null)
const [markedPaid, setMarkedPaid] = useState(false)
const [whatsappSent, setWhatsappSent] = useState(false)
const copyReference = async () => {
await navigator.clipboard.writeText(pledge.reference)
setCopied(true)
setTimeout(() => setCopied(false), 3000)
const bd = pledge.bankDetails
// Send bank details to WhatsApp
useEffect(() => {
if (!donorPhone || whatsappSent) return
fetch("/api/whatsapp/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "receipt",
phone: donorPhone,
data: {
amountPounds: (amount / 100).toFixed(0),
eventName,
reference: pledge.reference,
rail: "bank",
bankDetails: bd ? { sortCode: bd.sortCode, accountNo: bd.accountNo, accountName: bd.accountName } : undefined,
},
}),
}).then(() => setWhatsappSent(true)).catch(() => {})
}, [donorPhone, whatsappSent, amount, eventName, pledge.reference, bd])
const copyField = async (value: string, field: string) => {
await navigator.clipboard.writeText(value)
setCopiedField(field)
if (navigator.vibrate) navigator.vibrate(10)
setTimeout(() => setCopiedField(null), 2000)
// Track
fetch("/api/analytics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ eventType: "instruction_copy_clicked", pledgeId: pledge.id }),
body: JSON.stringify({ eventType: "instruction_copy_clicked", pledgeId: pledge.id, metadata: { field } }),
}).catch(() => {})
}
const handleIPaid = async () => {
setMarkedPaid(true)
if (navigator.vibrate) navigator.vibrate([10, 50, 10])
fetch(`/api/pledges/${pledge.id}/mark-initiated`, { method: "POST" }).catch(() => {})
fetch("/api/analytics", {
method: "POST",
@@ -46,23 +72,32 @@ export function BankInstructionsStep({ pledge, amount, eventName }: Props) {
}).catch(() => {})
}
const bd = pledge.bankDetails
// Post-payment view
if (markedPaid) {
return (
<div className="max-w-md mx-auto pt-8 text-center space-y-6">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success-green/10">
<Check className="h-10 w-10 text-success-green" />
<div className="max-w-md mx-auto pt-8 text-center space-y-6 animate-fade-up">
<div className="relative inline-flex items-center justify-center">
<div className="absolute w-20 h-20 rounded-full bg-success-green/20 animate-pulse-ring" />
<div className="relative w-20 h-20 rounded-full bg-gradient-to-br from-success-green to-emerald-500 flex items-center justify-center shadow-xl shadow-success-green/30">
<Check className="h-10 w-10 text-white" strokeWidth={3} />
</div>
</div>
<h1 className="text-2xl font-extrabold text-gray-900">Thank you!</h1>
<h1 className="text-2xl font-black text-gray-900">Thank you!</h1>
<p className="text-muted-foreground">
We&apos;ll confirm once your payment of <span className="font-bold text-foreground">£{(amount / 100).toFixed(2)}</span> is received.
We&apos;ll confirm once your <span className="font-bold text-foreground">£{(amount / 100).toFixed(0)}</span> is received.
</p>
{whatsappSent && (
<div className="rounded-xl bg-[#25D366]/10 border border-[#25D366]/20 p-3 text-sm text-[#25D366] font-medium flex items-center justify-center gap-2 animate-fade-in">
<MessageCircle className="h-4 w-4" /> Details sent to your WhatsApp
</div>
)}
<Card className="text-left">
<CardContent className="pt-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Reference</span>
<span className="font-mono font-bold">{pledge.reference}</span>
<span className="font-mono font-bold text-trust-blue">{pledge.reference}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Event</span>
@@ -70,26 +105,26 @@ export function BankInstructionsStep({ pledge, amount, eventName }: Props) {
</div>
</CardContent>
</Card>
{/* Share CTA */}
<div className="rounded-2xl bg-warm-amber/5 border border-warm-amber/20 p-4 space-y-3 text-center">
<p className="text-sm font-semibold text-gray-900">🤲 Know someone who&apos;d donate too?</p>
{/* Share */}
<div className="rounded-2xl bg-gradient-to-br from-warm-amber/5 to-orange-50 border border-warm-amber/20 p-5 space-y-3">
<div className="flex items-center justify-center gap-2">
<Sparkles className="h-4 w-4 text-warm-amber" />
<p className="text-sm font-bold text-gray-900">Know someone who&apos;d donate too?</p>
</div>
<div className="flex gap-2">
<Button
onClick={() => {
const text = `I just pledged £${(amount / 100).toFixed(0)} to ${eventName}! 🤲\nPledge here: ${window.location.origin}`
const text = `I just pledged £${(amount / 100).toFixed(0)} to ${eventName}! 🤲\nPledge here: ${window.location.href}`
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank")
}}
className="flex-1 bg-[#25D366] hover:bg-[#20BD5A] text-white"
className="flex-1 bg-[#25D366] hover:bg-[#20BD5A] text-white shadow-lg shadow-[#25D366]/25"
size="sm"
>
<MessageCircle className="h-4 w-4 mr-1" /> WhatsApp
</Button>
<Button
onClick={async () => {
if (navigator.share) {
await navigator.share({ title: eventName, text: `Pledge to ${eventName}`, url: window.location.origin })
}
}}
onClick={() => navigator.share?.({ title: eventName, text: `Pledge to ${eventName}`, url: window.location.href })}
variant="outline"
size="sm"
className="flex-1"
@@ -100,94 +135,108 @@ export function BankInstructionsStep({ pledge, amount, eventName }: Props) {
</div>
<p className="text-xs text-muted-foreground">
Need help? Contact the charity directly.
Payments usually arrive within 2 hours. We&apos;ll email you once confirmed.
</p>
</div>
)
}
// Copy-able field component
const CopyField = ({ label, value, fieldKey, mono }: { label: string; value: string; fieldKey: string; mono?: boolean }) => (
<button
onClick={() => copyField(value, fieldKey)}
className="flex items-center justify-between w-full py-2.5 px-0 group"
>
<div className="text-left">
<p className="text-[10px] text-muted-foreground uppercase tracking-wider font-medium">{label}</p>
<p className={`font-bold text-lg ${mono ? "font-mono" : ""} text-gray-900`}>{value}</p>
</div>
<div className={`rounded-lg px-2.5 py-1.5 text-xs font-medium transition-all ${
copiedField === fieldKey
? "bg-success-green text-white"
: "bg-gray-100 text-gray-500 group-hover:bg-trust-blue/10 group-hover:text-trust-blue"
}`}>
{copiedField === fieldKey ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</div>
</button>
)
return (
<div className="max-w-md mx-auto pt-4 space-y-6">
<div className="max-w-md mx-auto pt-2 space-y-5 animate-fade-up">
<div className="text-center space-y-2">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-trust-blue/10 mb-2">
<span className="text-3xl">🏦</span>
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gradient-to-br from-trust-blue to-blue-600 shadow-lg shadow-trust-blue/30">
<span className="text-2xl">🏦</span>
</div>
<h1 className="text-2xl font-extrabold text-gray-900">
Transfer £{(amount / 100).toFixed(2)}
<h1 className="text-2xl font-black text-gray-900 tracking-tight">
Transfer £{(amount / 100).toFixed(0)}
</h1>
<p className="text-muted-foreground">
Use these details in your banking app
<p className="text-sm text-muted-foreground">
Tap any field to copy · Use your banking app
</p>
</div>
{/* Bank details card */}
{whatsappSent && (
<div className="rounded-xl bg-[#25D366]/10 border border-[#25D366]/20 p-2.5 text-xs text-[#25D366] font-medium flex items-center justify-center gap-2 animate-fade-in">
<MessageCircle className="h-3.5 w-3.5" /> Bank details also sent to your WhatsApp
</div>
)}
{/* Bank details — tap to copy each field */}
{bd && (
<Card>
<CardContent className="pt-6 space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground text-xs uppercase tracking-wider">Sort Code</p>
<p className="font-mono font-bold text-lg">{bd.sortCode}</p>
</div>
<div>
<p className="text-muted-foreground text-xs uppercase tracking-wider">Account No</p>
<p className="font-mono font-bold text-lg">{bd.accountNo}</p>
</div>
</div>
<div className="text-sm">
<p className="text-muted-foreground text-xs uppercase tracking-wider">Account Name</p>
<p className="font-semibold">{bd.accountName}</p>
</div>
<Card className="overflow-hidden">
<div className="h-1 bg-gradient-to-r from-trust-blue to-blue-400" />
<CardContent className="pt-4 divide-y">
<CopyField label="Sort Code" value={bd.sortCode} fieldKey="sortCode" mono />
<CopyField label="Account Number" value={bd.accountNo} fieldKey="accountNo" mono />
<CopyField label="Account Name" value={bd.accountName} fieldKey="accountName" />
</CardContent>
</Card>
)}
{/* Reference - THE KEY */}
<div className="rounded-2xl border-2 border-trust-blue bg-trust-blue/5 p-6 text-center space-y-3">
<p className="text-xs font-semibold text-trust-blue uppercase tracking-wider">
Payment Reference use exactly:
</p>
<p className="text-3xl font-mono font-extrabold text-trust-blue tracking-wider">
{pledge.reference}
{/* THE reference — the most important thing */}
<div className="rounded-2xl border-2 border-trust-blue bg-gradient-to-br from-trust-blue/5 to-blue-50 p-5 text-center space-y-3">
<p className="text-xs font-bold text-trust-blue uppercase tracking-widest">
Payment Reference
</p>
<button
onClick={() => copyField(pledge.reference, "reference")}
className="group"
>
<p className="text-3xl font-mono font-black text-trust-blue tracking-wider group-hover:scale-105 transition-transform">
{pledge.reference}
</p>
</button>
<Button
onClick={copyReference}
variant={copied ? "success" : "default"}
size="lg"
onClick={() => copyField(pledge.reference, "reference")}
variant={copiedField === "reference" ? "success" : "default"}
className="w-full"
>
{copied ? (
<>
<Check className="h-5 w-5 mr-2" /> Copied!
</>
{copiedField === "reference" ? (
<><Check className="h-5 w-5 mr-2" /> Copied!</>
) : (
<>
<Copy className="h-5 w-5 mr-2" /> Copy Reference
</>
<><Copy className="h-5 w-5 mr-2" /> Copy Reference</>
)}
</Button>
<p className="text-[11px] text-trust-blue/70">
Use this exact reference so we can match your payment
</p>
</div>
{/* Suggestion */}
<div className="rounded-2xl bg-warm-amber/5 border border-warm-amber/20 p-4 text-center">
<p className="text-sm font-medium text-warm-amber">
<ExternalLink className="h-4 w-4 inline mr-1" />
Open your banking app now and search for &quot;new payment&quot;
{/* Open banking app hint */}
<div className="rounded-xl bg-warm-amber/5 border border-warm-amber/20 p-3 text-center">
<p className="text-xs font-medium text-warm-amber flex items-center justify-center gap-1.5">
<ExternalLink className="h-3.5 w-3.5" />
Open your banking app New payment Paste the details
</p>
</div>
{/* I've paid */}
<Button
size="xl"
variant="success"
className="w-full"
onClick={handleIPaid}
>
<Button size="xl" variant="success" className="w-full" onClick={handleIPaid}>
I&apos;ve Sent the Payment
</Button>
<p className="text-center text-xs text-muted-foreground">
Payments usually take 1-2 hours to arrive. We&apos;ll confirm once received.
Payments usually arrive within 2 hours. No rush transfer at your convenience.
</p>
</div>
)

View File

@@ -1,8 +1,9 @@
"use client"
import { Check, Share2, MessageCircle } from "lucide-react"
import { useEffect, useState, useCallback } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Check, Share2, MessageCircle, Copy, Sparkles } from "lucide-react"
interface Props {
pledge: { id: string; reference: string }
@@ -10,9 +11,49 @@ interface Props {
rail: string
eventName: string
shareUrl?: string
donorPhone?: string
}
export function ConfirmationStep({ pledge, amount, rail, eventName, shareUrl }: Props) {
// Mini confetti
function Confetti() {
const [pieces, setPieces] = useState<Array<{ x: number; color: string; delay: number; size: number }>>([])
useEffect(() => {
setPieces(
Array.from({ length: 40 }, () => ({
x: Math.random() * 100,
color: ["#1e40af", "#16a34a", "#f59e0b", "#ec4899", "#8b5cf6"][Math.floor(Math.random() * 5)],
delay: Math.random() * 2,
size: 4 + Math.random() * 8,
}))
)
}, [])
return (
<div className="fixed inset-0 pointer-events-none overflow-hidden z-50">
{pieces.map((p, i) => (
<div
key={i}
className="absolute top-0"
style={{
left: `${p.x}%`,
width: p.size,
height: p.size,
backgroundColor: p.color,
borderRadius: Math.random() > 0.5 ? "50%" : "2px",
animation: `confetti-fall ${2 + Math.random() * 2}s ease-in forwards`,
animationDelay: `${p.delay}s`,
}}
/>
))}
</div>
)
}
export function ConfirmationStep({ pledge, amount, rail, eventName, shareUrl, donorPhone }: Props) {
const [copied, setCopied] = useState(false)
const [whatsappSent, setWhatsappSent] = useState(false)
const railLabels: Record<string, string> = {
bank: "Bank Transfer",
gocardless: "Direct Debit",
@@ -21,10 +62,38 @@ export function ConfirmationStep({ pledge, amount, rail, eventName, shareUrl }:
const nextStepMessages: Record<string, string> = {
bank: "We've sent you payment instructions. Transfer at your convenience — we'll confirm once received.",
gocardless: "Your Direct Debit mandate has been set up. The payment of £" + (amount / 100).toFixed(2) + " will be collected automatically in 3-5 working days. You'll receive email confirmation from GoCardless. Protected by the Direct Debit Guarantee.",
card: "Your card payment has been processed. You'll receive a confirmation email shortly.",
gocardless: `Your Direct Debit mandate is set up. £${(amount / 100).toFixed(2)} will be collected automatically in 3-5 working days. Protected by the Direct Debit Guarantee.`,
card: "Your card payment has been processed. Confirmation email is on its way.",
}
// Send WhatsApp receipt if phone provided
const sendWhatsAppReceipt = useCallback(async () => {
if (!donorPhone || whatsappSent) return
try {
await fetch("/api/whatsapp/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "receipt",
phone: donorPhone,
data: {
amountPounds: (amount / 100).toFixed(0),
eventName,
reference: pledge.reference,
rail,
},
}),
})
setWhatsappSent(true)
} catch {
// Silent fail — not critical
}
}, [donorPhone, whatsappSent, amount, eventName, pledge.reference, rail])
useEffect(() => {
sendWhatsAppReceipt()
}, [sendWhatsAppReceipt])
const handleWhatsAppShare = () => {
const text = `I just pledged £${(amount / 100).toFixed(0)} to ${eventName}! 🤲\n\nYou can pledge too: ${shareUrl || window.location.origin}`
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank")
@@ -39,89 +108,101 @@ export function ConfirmationStep({ pledge, amount, rail, eventName, shareUrl }:
}
}
const copyRef = async () => {
await navigator.clipboard.writeText(pledge.reference)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="max-w-md mx-auto pt-8 text-center space-y-6">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success-green/10">
<Check className="h-10 w-10 text-success-green" />
</div>
<div className="space-y-2">
<h1 className="text-2xl font-extrabold text-gray-900">
{rail === "gocardless" ? "Mandate Set Up!" : rail === "card" ? "Payment Complete!" : "Pledge Received!"}
</h1>
<p className="text-muted-foreground">
Thank you for your generous {rail === "card" ? "donation" : "pledge"} to{" "}
<span className="font-semibold text-foreground">{eventName}</span>
</p>
</div>
<Card>
<CardContent className="pt-6 space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Amount</span>
<span className="font-bold">£{(amount / 100).toFixed(2)}</span>
<>
<Confetti />
<div className="max-w-md mx-auto pt-6 text-center space-y-6 animate-fade-up">
{/* Success icon with pulse */}
<div className="relative inline-flex items-center justify-center">
<div className="absolute w-20 h-20 rounded-full bg-success-green/20 animate-pulse-ring" />
<div className="relative w-20 h-20 rounded-full bg-gradient-to-br from-success-green to-emerald-500 flex items-center justify-center shadow-xl shadow-success-green/30">
<Check className="h-10 w-10 text-white" strokeWidth={3} />
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Payment Method</span>
<span>{railLabels[rail] || rail}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Reference</span>
<span className="font-mono font-bold">{pledge.reference}</span>
</div>
{rail === "gocardless" && (
<div className="flex justify-between">
<span className="text-muted-foreground">Collection</span>
<span className="text-sm">3-5 working days</span>
</div>
)}
{rail === "card" && (
<div className="flex justify-between">
<span className="text-muted-foreground">Status</span>
<span className="text-success-green font-semibold">Paid </span>
</div>
)}
</CardContent>
</Card>
{/* What happens next */}
<div className="rounded-2xl bg-trust-blue/5 border border-trust-blue/20 p-4 space-y-2">
<p className="text-sm font-medium text-trust-blue">What happens next?</p>
<p className="text-sm text-muted-foreground">
{nextStepMessages[rail] || nextStepMessages.bank}
</p>
</div>
{/* Share / encourage others */}
<div className="rounded-2xl bg-warm-amber/5 border border-warm-amber/20 p-5 space-y-3">
<p className="text-sm font-semibold text-gray-900">
🤲 Spread the word every pledge counts!
</p>
<p className="text-xs text-muted-foreground">
Share with friends and family so they can pledge too.
</p>
<div className="flex gap-2">
<Button
onClick={handleWhatsAppShare}
className="flex-1 bg-[#25D366] hover:bg-[#20BD5A] text-white"
>
<MessageCircle className="h-4 w-4 mr-2" />
WhatsApp
</Button>
<Button
onClick={handleShare}
variant="outline"
className="flex-1"
>
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Need help? Contact the charity directly. Ref: {pledge.reference}
</p>
</div>
<div className="space-y-2">
<h1 className="text-2xl font-black text-gray-900">
{rail === "card" ? "Payment Complete!" : rail === "gocardless" ? "Mandate Set Up!" : "Pledge Received!"}
</h1>
<p className="text-muted-foreground">
Thank you for your generous support of{" "}
<span className="font-semibold text-foreground">{eventName}</span>
</p>
</div>
{/* Details card */}
<Card className="text-left overflow-hidden">
<div className="h-1 bg-gradient-to-r from-trust-blue via-success-green to-warm-amber" />
<CardContent className="pt-5 space-y-3 text-sm">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Amount</span>
<span className="font-black text-lg">£{(amount / 100).toFixed(2)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Method</span>
<span className="font-medium">{railLabels[rail] || rail}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Reference</span>
<button onClick={copyRef} className="font-mono font-bold text-trust-blue flex items-center gap-1.5 hover:underline">
{pledge.reference}
{copied ? <Check className="h-3.5 w-3.5 text-success-green" /> : <Copy className="h-3.5 w-3.5" />}
</button>
</div>
{rail === "card" && (
<div className="flex justify-between items-center pt-1 border-t">
<span className="text-muted-foreground">Status</span>
<span className="text-success-green font-bold flex items-center gap-1">
<Check className="h-4 w-4" /> Paid
</span>
</div>
)}
</CardContent>
</Card>
{/* What happens next */}
<div className="rounded-2xl bg-trust-blue/5 border border-trust-blue/10 p-4 text-left">
<p className="text-sm font-semibold text-trust-blue mb-1">What happens next?</p>
<p className="text-sm text-muted-foreground">{nextStepMessages[rail] || nextStepMessages.bank}</p>
</div>
{whatsappSent && (
<div className="rounded-xl bg-[#25D366]/10 border border-[#25D366]/20 p-3 text-sm text-[#25D366] font-medium flex items-center justify-center gap-2 animate-fade-in">
<MessageCircle className="h-4 w-4" /> Receipt sent to your WhatsApp
</div>
)}
{/* Share */}
<div className="rounded-2xl bg-gradient-to-br from-warm-amber/5 to-orange-50 border border-warm-amber/20 p-5 space-y-3">
<div className="flex items-center justify-center gap-2">
<Sparkles className="h-4 w-4 text-warm-amber" />
<p className="text-sm font-bold text-gray-900">
Double your impact share with friends
</p>
</div>
<p className="text-xs text-muted-foreground">
Every share can inspire another pledge
</p>
<div className="flex gap-2">
<Button onClick={handleWhatsAppShare} className="flex-1 bg-[#25D366] hover:bg-[#20BD5A] text-white shadow-lg shadow-[#25D366]/25">
<MessageCircle className="h-4 w-4 mr-2" /> WhatsApp
</Button>
<Button onClick={handleShare} variant="outline" className="flex-1">
<Share2 className="h-4 w-4 mr-2" /> Share
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground pb-4">
Need help? Contact the charity directly · Ref: {pledge.reference}
</p>
</div>
</>
)
}

View File

@@ -1,10 +1,8 @@
"use client"
import { useState } from "react"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Gift, Shield } from "lucide-react"
import { Gift, Shield, Sparkles, Phone, Mail } from "lucide-react"
interface Props {
onSubmit: (data: {
@@ -22,10 +20,15 @@ export function IdentityStep({ onSubmit, amount }: Props) {
const [phone, setPhone] = useState("")
const [giftAid, setGiftAid] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [contactMode, setContactMode] = useState<"email" | "phone">("email")
const nameRef = useRef<HTMLInputElement>(null)
const hasContact = email.includes("@") || phone.length >= 10
useEffect(() => { nameRef.current?.focus() }, [])
const hasContact = contactMode === "email" ? email.includes("@") : phone.length >= 10
const isValid = hasContact
const giftAidBonus = Math.round(amount * 0.25)
const totalWithAid = amount + giftAidBonus
const handleSubmit = async () => {
if (!isValid) return
@@ -38,123 +41,168 @@ export function IdentityStep({ onSubmit, amount }: Props) {
}
return (
<div className="max-w-md mx-auto pt-4 space-y-6">
<div className="max-w-md mx-auto pt-2 space-y-5 animate-fade-up">
<div className="text-center space-y-2">
<h1 className="text-2xl font-extrabold text-gray-900">
<h1 className="text-2xl font-black text-gray-900 tracking-tight">
Almost there!
</h1>
<p className="text-muted-foreground">
We need a way to send you payment details
<p className="text-muted-foreground text-sm">
We just need a way to send you payment details
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name <span className="text-muted-foreground font-normal">(for Gift Aid)</span></Label>
<Input
id="name"
placeholder="Your full name"
{/* Minimal form */}
<div className="space-y-3">
{/* Name */}
<div className="relative">
<input
ref={nameRef}
type="text"
placeholder="Your name"
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete="name"
className="w-full h-14 px-4 rounded-2xl border-2 border-gray-200 bg-white text-base font-medium placeholder:text-gray-300 focus:border-trust-blue focus:ring-4 focus:ring-trust-blue/10 outline-none transition-all"
/>
{name && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-success-green animate-scale-in"></div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
inputMode="email"
/>
<p className="text-xs text-muted-foreground">
We&apos;ll send your payment instructions and receipt here
</p>
{/* Contact mode toggle */}
<div className="flex rounded-xl bg-gray-100 p-1">
<button
onClick={() => setContactMode("email")}
className={`flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-medium transition-all ${
contactMode === "email" ? "bg-white shadow-sm text-gray-900" : "text-gray-500"
}`}
>
<Mail className="h-4 w-4" /> Email
</button>
<button
onClick={() => setContactMode("phone")}
className={`flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-medium transition-all ${
contactMode === "phone" ? "bg-white shadow-sm text-gray-900" : "text-gray-500"
}`}
>
<Phone className="h-4 w-4" /> Mobile
</button>
</div>
<div className="relative flex items-center">
<div className="flex-grow border-t border-gray-200" />
<span className="flex-shrink mx-3 text-xs text-muted-foreground">or</span>
<div className="flex-grow border-t border-gray-200" />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Mobile Number</Label>
<Input
id="phone"
type="tel"
placeholder="07700 900000"
value={phone}
onChange={(e) => setPhone(e.target.value)}
autoComplete="tel"
inputMode="tel"
/>
<p className="text-xs text-muted-foreground">
We can send reminders via SMS if you prefer
</p>
</div>
{/* Gift Aid — prominent UK-specific */}
<div
onClick={() => setGiftAid(!giftAid)}
className={`rounded-2xl border-2 p-5 cursor-pointer transition-all ${
giftAid
? "border-success-green bg-success-green/5 shadow-md shadow-success-green/10"
: "border-gray-200 bg-white hover:border-success-green/50"
}`}
>
<div className="flex items-start gap-4">
<div className={`rounded-xl p-2.5 ${giftAid ? "bg-success-green/10" : "bg-gray-100"}`}>
<Gift className={`h-6 w-6 ${giftAid ? "text-success-green" : "text-gray-400"}`} />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={giftAid}
onChange={() => {}}
className="h-5 w-5 rounded border-gray-300 text-success-green focus:ring-success-green"
/>
<span className="font-bold text-gray-900">Add Gift Aid</span>
{giftAid && (
<span className="text-xs font-bold px-2 py-0.5 rounded-full bg-success-green text-white">
+£{(giftAidBonus / 100).toFixed(0)} free
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">
Boost your £{(amount / 100).toFixed(0)} pledge to{" "}
<span className="font-bold text-success-green">£{((amount + giftAidBonus) / 100).toFixed(0)}</span> at no extra cost.
HMRC adds 25% the charity claims it back.
</p>
{giftAid && (
<p className="text-xs text-muted-foreground mt-2 italic">
I confirm I am a UK taxpayer and understand that if I pay less Income Tax and/or
Capital Gains Tax than the amount of Gift Aid claimed on all my donations in that
tax year it is my responsibility to pay any difference.
</p>
)}
</div>
{/* Contact input */}
{contactMode === "email" ? (
<div className="relative animate-fade-in">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-300" />
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
inputMode="email"
className="w-full h-14 pl-12 pr-4 rounded-2xl border-2 border-gray-200 bg-white text-base font-medium placeholder:text-gray-300 focus:border-trust-blue focus:ring-4 focus:ring-trust-blue/10 outline-none transition-all"
/>
</div>
</div>
) : (
<div className="relative animate-fade-in">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-300" />
<input
type="tel"
placeholder="07700 900 000"
value={phone}
onChange={(e) => setPhone(e.target.value)}
autoComplete="tel"
inputMode="tel"
className="w-full h-14 pl-12 pr-4 rounded-2xl border-2 border-gray-200 bg-white text-base font-medium placeholder:text-gray-300 focus:border-trust-blue focus:ring-4 focus:ring-trust-blue/10 outline-none transition-all"
/>
<p className="text-xs text-muted-foreground mt-1 ml-1">
We&apos;ll send reminders via WhatsApp
</p>
</div>
)}
</div>
{/* Gift Aid — the hero */}
<button
onClick={() => setGiftAid(!giftAid)}
className={`w-full text-left rounded-2xl border-2 p-5 transition-all duration-300 card-hover ${
giftAid
? "border-success-green bg-gradient-to-br from-success-green/5 to-emerald-50 shadow-lg shadow-success-green/10"
: "border-gray-200 bg-white hover:border-success-green/40"
}`}
>
<div className="flex items-start gap-4">
<div className={`rounded-xl p-3 transition-all ${giftAid ? "bg-success-green shadow-lg shadow-success-green/30" : "bg-gray-100"}`}>
<Gift className={`h-6 w-6 transition-colors ${giftAid ? "text-white" : "text-gray-400"}`} />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-gray-900">
{giftAid ? "Gift Aid added!" : "Add Gift Aid"}
</span>
{giftAid ? (
<span className="text-xs font-bold px-2.5 py-0.5 rounded-full bg-success-green text-white animate-scale-in flex items-center gap-1">
<Sparkles className="h-3 w-3" /> +£{(giftAidBonus / 100).toFixed(0)} free
</span>
) : (
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-success-green/10 text-success-green">
+25%
</span>
)}
</div>
{giftAid ? (
<div className="mt-2 space-y-2 animate-fade-in">
<div className="flex items-center justify-between bg-white rounded-xl p-3 border border-success-green/20">
<div>
<p className="text-xs text-muted-foreground">Your pledge</p>
<p className="font-bold">£{(amount / 100).toFixed(0)}</p>
</div>
<div className="text-success-green font-bold text-xl">+</div>
<div>
<p className="text-xs text-muted-foreground">HMRC adds</p>
<p className="font-bold text-success-green">£{(giftAidBonus / 100).toFixed(0)}</p>
</div>
<div className="text-success-green font-bold text-xl">=</div>
<div>
<p className="text-xs text-muted-foreground">Charity gets</p>
<p className="font-black text-success-green text-lg">£{(totalWithAid / 100).toFixed(0)}</p>
</div>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
I confirm I am a UK taxpayer and understand that if I pay less Income Tax and/or Capital Gains Tax than the amount of Gift Aid claimed on all my donations in that tax year, it is my responsibility to pay any difference.
</p>
</div>
) : (
<p className="text-sm text-muted-foreground mt-1">
Boost your £{(amount / 100).toFixed(0)} to{" "}
<span className="font-bold text-success-green">£{(totalWithAid / 100).toFixed(0)}</span> at zero cost. HMRC adds 25%.
</p>
)}
</div>
</div>
</button>
{/* Submit */}
<Button
size="xl"
className="w-full"
className={`w-full transition-all duration-300 ${isValid ? "opacity-100" : "opacity-50"}`}
disabled={!isValid || submitting}
onClick={handleSubmit}
>
{submitting ? "Submitting..." : "Complete Pledge ✓"}
{submitting ? (
<span className="flex items-center gap-2">
<span className="h-5 w-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
Submitting...
</span>
) : (
"Complete Pledge ✓"
)}
</Button>
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
<Shield className="h-3 w-3" />
<span>Your data is kept secure and only used for this pledge</span>
<span>Your data is encrypted and only used for this pledge</span>
</div>
</div>
)

View File

@@ -1,6 +1,6 @@
"use client"
import { Building2, CreditCard, Landmark } from "lucide-react"
import { Building2, CreditCard, Landmark, Shield, CheckCircle2 } from "lucide-react"
interface Props {
onSelect: (rail: "bank" | "gocardless" | "card") => void
@@ -8,90 +8,115 @@ interface Props {
}
export function PaymentStep({ onSelect, amount }: Props) {
const pounds = (amount / 100).toFixed(2)
const pounds = (amount / 100).toFixed(0)
const giftAidTotal = ((amount + amount * 0.25) / 100).toFixed(0)
const options = [
{
id: "bank" as const,
icon: Building2,
title: "Bank Transfer",
subtitle: "Zero fees — 100% goes to charity",
subtitle: "100% goes to charity — zero fees",
tag: "Recommended",
tagColor: "bg-success-green text-white",
detail: "Use your banking app to transfer directly. We'll give you the details.",
fee: "No fees",
feeColor: "text-success-green",
tagClass: "bg-success-green text-white",
detail: "We'll give you the bank details. Transfer in your own time.",
fee: "Free",
feeClass: "text-success-green font-bold",
iconBg: "from-emerald-500 to-green-600",
highlight: true,
benefits: ["Zero fees", "Most charities prefer this"],
},
{
id: "gocardless" as const,
icon: Landmark,
title: "Direct Debit",
subtitle: "Automatic collection — set and forget",
tag: "Set up once",
tagColor: "bg-trust-blue/10 text-trust-blue",
detail: "We'll collect via GoCardless. Protected by the Direct Debit Guarantee.",
tag: "Hassle-free",
tagClass: "bg-trust-blue/10 text-trust-blue",
detail: "GoCardless collects it for you. Protected by the DD Guarantee.",
fee: "1% + 20p",
feeColor: "text-muted-foreground",
feeClass: "text-muted-foreground",
iconBg: "from-trust-blue to-blue-600",
highlight: false,
benefits: ["No action needed", "DD Guarantee"],
},
{
id: "card" as const,
icon: CreditCard,
title: "Debit or Credit Card",
subtitle: "Pay instantly by Visa, Mastercard, or Amex",
title: "Card Payment",
subtitle: "Visa, Mastercard, Amex — instant",
tag: "Instant",
tagColor: "bg-purple-100 text-purple-700",
detail: "Secure payment powered by Stripe. Receipt emailed immediately.",
tagClass: "bg-purple-100 text-purple-700",
detail: "Powered by Stripe. Receipt emailed instantly.",
fee: "1.4% + 20p",
feeColor: "text-muted-foreground",
feeClass: "text-muted-foreground",
iconBg: "from-purple-500 to-violet-600",
highlight: false,
benefits: ["Instant confirmation", "All major cards"],
},
]
return (
<div className="max-w-md mx-auto pt-4 space-y-6">
<div className="max-w-md mx-auto pt-2 space-y-6 animate-fade-up">
<div className="text-center space-y-2">
<h1 className="text-2xl font-extrabold text-gray-900">
<h1 className="text-2xl font-black text-gray-900 tracking-tight">
How would you like to pay?
</h1>
<p className="text-lg text-muted-foreground">
<p className="text-base text-muted-foreground">
Your pledge: <span className="font-bold text-foreground">£{pounds}</span>
<span className="text-success-green text-xs ml-1">(£{giftAidTotal} with Gift Aid)</span>
</p>
</div>
<div className="space-y-3">
<div className="space-y-3 stagger-children">
{options.map((opt) => (
<button
key={opt.id}
onClick={() => onSelect(opt.id)}
className="w-full text-left tap-target rounded-2xl border-2 border-gray-200 bg-white p-5 hover:border-trust-blue/50 active:scale-[0.99] transition-all group"
className={`
w-full text-left rounded-2xl border-2 bg-white p-5 transition-all duration-200 group card-hover
${opt.highlight
? "border-success-green/40 shadow-sm shadow-success-green/10 hover:border-success-green hover:shadow-lg hover:shadow-success-green/15"
: "border-gray-200 hover:border-trust-blue/40 hover:shadow-lg hover:shadow-trust-blue/10"
}
`}
>
<div className="flex items-start gap-4">
<div className="rounded-xl bg-trust-blue/5 p-3 group-hover:bg-trust-blue/10 transition-colors">
<opt.icon className="h-6 w-6 text-trust-blue" />
<div className={`rounded-xl bg-gradient-to-br ${opt.iconBg} p-3 shadow-lg shadow-trust-blue/10 group-hover:scale-105 transition-transform`}>
<opt.icon className="h-5 w-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-bold text-gray-900">{opt.title}</span>
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${opt.tagColor}`}>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${opt.tagClass}`}>
{opt.tag}
</span>
</div>
<p className="text-sm text-muted-foreground mt-0.5">{opt.subtitle}</p>
<p className="text-xs text-muted-foreground/70 mt-1">{opt.detail}</p>
<p className={`text-xs font-medium mt-1 ${opt.feeColor}`}>
Fee: {opt.fee}
</p>
</div>
<div className="text-muted-foreground/40 group-hover:text-trust-blue transition-colors text-xl">
<div className="flex items-center gap-3 mt-2">
{opt.benefits.map((b, i) => (
<span key={i} className="text-[11px] text-muted-foreground inline-flex items-center gap-1">
<CheckCircle2 className="h-3 w-3 text-success-green" />
{b}
</span>
))}
</div>
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
<span className={`text-xs ${opt.feeClass}`}>Fee: {opt.fee}</span>
<span className="text-xs text-trust-blue font-medium opacity-0 group-hover:opacity-100 transition-opacity">
Select
</span>
</div>
</div>
</div>
</button>
))}
</div>
<p className="text-center text-xs text-muted-foreground">
All payments are secure. Bank transfers mean 100% reaches the charity.
</p>
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground animate-fade-in" style={{ animationDelay: "300ms" }}>
<Shield className="h-3.5 w-3.5" />
<span>All payments are encrypted and secure</span>
</div>
</div>
)
}