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:
@@ -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't be charged now. Choose how to pay next.
|
||||
No payment now — choose how to pay on the next step
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user