feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Lock, Search, CheckCircle2 } from "lucide-react"
|
||||
|
||||
interface Props {
|
||||
amount: number
|
||||
eventName: string
|
||||
onComplete: (identity: {
|
||||
donorName: string
|
||||
donorEmail: string
|
||||
donorPhone: string
|
||||
giftAid: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
interface Bank {
|
||||
code: string
|
||||
name: string
|
||||
shortName: string
|
||||
online: boolean
|
||||
}
|
||||
|
||||
const FPX_BANKS: Bank[] = [
|
||||
{ code: "MBB", name: "Maybank2u", shortName: "Maybank", online: true },
|
||||
{ code: "CIMB", name: "CIMB Clicks", shortName: "CIMB", online: true },
|
||||
{ code: "PBB", name: "PBe Bank", shortName: "Public Bank", online: true },
|
||||
{ code: "RHB", name: "RHB Now", shortName: "RHB", online: true },
|
||||
{ code: "HLB", name: "Hong Leong Connect", shortName: "Hong Leong", online: true },
|
||||
{ code: "AMBB", name: "AmOnline", shortName: "AmBank", online: true },
|
||||
{ code: "BIMB", name: "Bank Islam GO", shortName: "Bank Islam", online: true },
|
||||
{ code: "BKRM", name: "i-Rakyat", shortName: "Bank Rakyat", online: true },
|
||||
{ code: "BSN", name: "myBSN", shortName: "BSN", online: true },
|
||||
{ code: "OCBC", name: "OCBC Online", shortName: "OCBC", online: true },
|
||||
{ code: "UOB", name: "UOB Personal", shortName: "UOB", online: true },
|
||||
{ code: "ABB", name: "Affin Online", shortName: "Affin Bank", online: true },
|
||||
{ code: "ABMB", name: "Alliance Online", shortName: "Alliance Bank", online: true },
|
||||
{ code: "BMMB", name: "Bank Muamalat", shortName: "Muamalat", online: true },
|
||||
{ code: "SCB", name: "SC Online", shortName: "Standard Chartered", online: true },
|
||||
{ code: "HSBC", name: "HSBC Online", shortName: "HSBC", online: true },
|
||||
{ code: "AGR", name: "AGRONet", shortName: "Agrobank", online: true },
|
||||
{ code: "KFH", name: "KFH Online", shortName: "KFH", online: true },
|
||||
]
|
||||
|
||||
const BANK_COLORS: Record<string, string> = {
|
||||
MBB: "bg-yellow-500",
|
||||
CIMB: "bg-red-600",
|
||||
PBB: "bg-pink-700",
|
||||
RHB: "bg-blue-800",
|
||||
HLB: "bg-blue-600",
|
||||
AMBB: "bg-green-700",
|
||||
BIMB: "bg-emerald-700",
|
||||
BKRM: "bg-blue-900",
|
||||
BSN: "bg-orange-600",
|
||||
OCBC: "bg-red-700",
|
||||
UOB: "bg-blue-700",
|
||||
ABB: "bg-amber-700",
|
||||
ABMB: "bg-teal-700",
|
||||
BMMB: "bg-green-800",
|
||||
SCB: "bg-green-600",
|
||||
HSBC: "bg-red-500",
|
||||
AGR: "bg-green-900",
|
||||
KFH: "bg-yellow-700",
|
||||
}
|
||||
|
||||
type Phase = "select" | "identity" | "redirecting" | "processing"
|
||||
|
||||
export function FpxPaymentStep({ amount, eventName, onComplete }: Props) {
|
||||
const [phase, setPhase] = useState<Phase>("select")
|
||||
const [selectedBank, setSelectedBank] = useState<Bank | null>(null)
|
||||
const [search, setSearch] = useState("")
|
||||
const [name, setName] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const ringgit = (amount / 100).toFixed(2)
|
||||
|
||||
const filteredBanks = search
|
||||
? FPX_BANKS.filter(
|
||||
(b) =>
|
||||
b.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
b.shortName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
b.code.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: FPX_BANKS
|
||||
|
||||
const handleBankSelect = (bank: Bank) => {
|
||||
setSelectedBank(bank)
|
||||
}
|
||||
|
||||
const handleContinueToIdentity = () => {
|
||||
if (!selectedBank) return
|
||||
setPhase("identity")
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errs: Record<string, string> = {}
|
||||
if (!email.includes("@")) errs.email = "Valid email required"
|
||||
setErrors(errs)
|
||||
if (Object.keys(errs).length > 0) return
|
||||
|
||||
setPhase("redirecting")
|
||||
|
||||
// Simulate FPX redirect flow
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
setPhase("processing")
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
|
||||
onComplete({
|
||||
donorName: name,
|
||||
donorEmail: email,
|
||||
donorPhone: phone,
|
||||
giftAid: false, // Gift Aid not applicable for MYR
|
||||
})
|
||||
}
|
||||
|
||||
// Redirecting phase
|
||||
if (phase === "redirecting") {
|
||||
return (
|
||||
<div className="max-w-md mx-auto pt-16 text-center space-y-6">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-trust-blue/10">
|
||||
<div className="h-10 w-10 border-4 border-trust-blue border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-extrabold text-gray-900">
|
||||
Redirecting to {selectedBank?.name}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
You'll be taken to your bank's secure login page to authorize the payment of <span className="font-bold text-foreground">RM{ringgit}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-gray-50 border p-4">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg ${BANK_COLORS[selectedBank?.code || ""] || "bg-gray-500"} flex items-center justify-center`}>
|
||||
<span className="text-white font-bold text-xs">{selectedBank?.code}</span>
|
||||
</div>
|
||||
<span className="font-semibold">{selectedBank?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Do not close this window. You will be redirected back automatically.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Processing phase
|
||||
if (phase === "processing") {
|
||||
return (
|
||||
<div className="max-w-md mx-auto pt-16 text-center space-y-6">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success-green/10">
|
||||
<div className="h-10 w-10 border-4 border-success-green border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-extrabold text-gray-900">
|
||||
Processing Payment
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Verifying your payment with {selectedBank?.shortName}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Identity phase
|
||||
if (phase === "identity") {
|
||||
return (
|
||||
<div className="max-w-md mx-auto pt-4 space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-2xl font-extrabold text-gray-900">Your Details</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Before we redirect you to <span className="font-semibold text-foreground">{selectedBank?.name}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Selected bank indicator */}
|
||||
<div className="rounded-2xl border-2 border-trust-blue/20 bg-trust-blue/5 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg ${BANK_COLORS[selectedBank?.code || ""] || "bg-gray-500"} flex items-center justify-center`}>
|
||||
<span className="text-white font-bold text-xs">{selectedBank?.code}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-sm">{selectedBank?.name}</p>
|
||||
<p className="text-xs text-muted-foreground">FPX Online Banking</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-lg">RM{ringgit}</p>
|
||||
<p className="text-xs text-muted-foreground">{eventName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border-2 border-gray-200 bg-white p-5 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fpx-name">Full Name <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
||||
<Input
|
||||
id="fpx-name"
|
||||
placeholder="Your full name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoComplete="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fpx-email">Email</Label>
|
||||
<Input
|
||||
id="fpx-email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
className={errors.email ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
|
||||
<p className="text-xs text-muted-foreground">We'll send your receipt here</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fpx-phone">Phone <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
||||
<Input
|
||||
id="fpx-phone"
|
||||
type="tel"
|
||||
placeholder="+60 12-345 6789"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="xl" className="w-full" onClick={handleSubmit}>
|
||||
<Lock className="h-5 w-5 mr-2" />
|
||||
Pay RM{ringgit} via {selectedBank?.shortName}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<Lock className="h-3 w-3" />
|
||||
<span>Secured by FPX — Bank Negara Malaysia</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Bank selection phase (default)
|
||||
return (
|
||||
<div className="max-w-md mx-auto pt-4 space-y-5">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-2xl font-extrabold text-gray-900">
|
||||
FPX Online Banking
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Pay <span className="font-bold text-foreground">RM{ringgit}</span>{" "}
|
||||
for <span className="font-semibold text-foreground">{eventName}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Search your bank..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bank list */}
|
||||
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto pr-1">
|
||||
{filteredBanks.map((bank) => (
|
||||
<button
|
||||
key={bank.code}
|
||||
onClick={() => handleBankSelect(bank)}
|
||||
className={`
|
||||
text-left rounded-xl border-2 p-3 transition-all active:scale-[0.98]
|
||||
${selectedBank?.code === bank.code
|
||||
? "border-trust-blue bg-trust-blue/5 shadow-md shadow-trust-blue/10"
|
||||
: "border-gray-200 bg-white hover:border-gray-300"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className={`w-9 h-9 rounded-lg ${BANK_COLORS[bank.code] || "bg-gray-500"} flex items-center justify-center flex-shrink-0`}>
|
||||
<span className="text-white font-bold text-[10px] leading-none">{bank.code}</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-semibold text-xs text-gray-900 truncate">{bank.shortName}</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">{bank.name}</p>
|
||||
</div>
|
||||
{selectedBank?.code === bank.code && (
|
||||
<CheckCircle2 className="h-4 w-4 text-trust-blue flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredBanks.length === 0 && (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">No banks found matching "{search}"</p>
|
||||
)}
|
||||
|
||||
{/* Continue */}
|
||||
<Button
|
||||
size="xl"
|
||||
className="w-full"
|
||||
disabled={!selectedBank}
|
||||
onClick={handleContinueToIdentity}
|
||||
>
|
||||
Continue with {selectedBank?.shortName || "selected bank"} →
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<Lock className="h-3 w-3" />
|
||||
<span>Powered by FPX — regulated by Bank Negara Malaysia</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user