full terminology overhaul + zakat fund types + fund allocation

POSITIONING FIX — PNPL is NOT just 'QR codes at events':
- Charities collecting at events (QR per table)
- High-net-worth donor outreach (personal links via WhatsApp/email)
- Org-to-org pledges (multi-charity projects)
- Personal fundraisers (LaunchGood/Enthuse redirect)

TERMINOLOGY (throughout app):
- Events → Campaigns (sidebar, pages, create dialogs, onboarding)
- QR Codes page → Pledge Links (sharing-first, QR is one option)
- Scans → Clicks (not just QR scans)
- 'New Event' → 'New Campaign'
- 'Create QR Code' → 'Create Pledge Link'
- Source label: 'Table Name' → 'Source / Channel'

SHARING (pledge links page):
- 4-button share row: Copy · WhatsApp · Email · More (native share)
- Each link shows its full URL
- Create dialog suggests: 'WhatsApp Family Group, Table 5, Instagram Bio'
- QR code is still shown but as one option, not the hero

LANDING PAGE (complete rewrite):
- Hero: 'Collect pledges. Convert them into donations.'
- 4 use case cards: Events, HNW Donors, Org-to-Org, Personal Fundraisers
- 'Share anywhere' section: WhatsApp, QR, Email, Instagram, Twitter, 1-on-1
- Platform support: Bank Transfer, LaunchGood, Enthuse, JustGiving, GoFundMe, Any URL
- Islamic fund types section: Zakat, Sadaqah, Sadaqah Jariyah, Lillah, Fitrana

ZAKAT & FUND TYPES:
- Organization.zakatEnabled toggle in Settings
- Pledge.fundType: general, zakat, sadaqah, lillah, fitrana
- Identity step: fund type picker (5 options) when org has zakatEnabled
- Zakat note: Quran 9:60 categories reference
- Settings: toggle card with fund type descriptions

FUND ALLOCATION:
- Event.fundAllocation: 'Mosque Building Fund', 'Orphan Sponsorship' etc.
- Charities can also add external URL for reference/allocation (not just fundraisers)
- Shows on campaign cards and pledge flow
This commit is contained in:
2026-03-03 07:00:04 +08:00
parent 0e8df76f89
commit f87aec7beb
17 changed files with 486 additions and 202 deletions

View File

@@ -21,6 +21,7 @@ export interface PledgeData {
donorEmail: string
donorPhone: string
giftAid: boolean
fundType?: string
// Scheduling
scheduleMode: "now" | "date" | "installments"
dueDate?: string
@@ -37,6 +38,8 @@ interface EventInfo {
paymentMode: "self" | "external"
externalUrl: string | null
externalPlatform: string | null
zakatEnabled: boolean
fundAllocation: string | null
}
/*
@@ -134,7 +137,7 @@ export default function PledgePage() {
}
// Submit pledge (from identity step, or card/DD steps)
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean }) => {
const submitPledge = async (identity: { donorName: string; donorEmail: string; donorPhone: string; giftAid: boolean; fundType?: string }) => {
const finalData = { ...pledgeData, ...identity }
setPledgeData(finalData)
@@ -146,6 +149,7 @@ export default function PledgePage() {
...finalData,
eventId: eventInfo?.id,
qrSourceId: eventInfo?.qrSourceId,
fundType: finalData.fundType || undefined,
}),
})
const result = await res.json()
@@ -205,7 +209,7 @@ export default function PledgePage() {
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} />,
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEnabled={eventInfo?.zakatEnabled} fundAllocation={eventInfo?.fundAllocation} />,
4: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
5: pledgeResult && (
<ConfirmationStep

View File

@@ -4,21 +4,33 @@ import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Gift, Shield, Sparkles, Phone, Mail } from "lucide-react"
const FUND_TYPES = [
{ id: "general", label: "General Donation", icon: "🤲", desc: "Sadaqah — used where most needed" },
{ id: "zakat", label: "Zakat", icon: "🌙", desc: "Obligatory annual charity (2.5%)" },
{ id: "sadaqah", label: "Sadaqah Jariyah", icon: "🌱", desc: "Ongoing charity — builds, wells, education" },
{ id: "lillah", label: "Lillah", icon: "🕌", desc: "For the mosque / institution itself" },
{ id: "fitrana", label: "Fitrana", icon: "🍽️", desc: "Zakat al-Fitr — given before Eid" },
]
interface Props {
onSubmit: (data: {
donorName: string
donorEmail: string
donorPhone: string
giftAid: boolean
fundType?: string
}) => void
amount: number
zakatEnabled?: boolean
fundAllocation?: string | null
}
export function IdentityStep({ onSubmit, amount }: Props) {
export function IdentityStep({ onSubmit, amount, zakatEnabled, fundAllocation }: Props) {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [phone, setPhone] = useState("")
const [giftAid, setGiftAid] = useState(false)
const [fundType, setFundType] = useState<string>(fundAllocation ? "general" : "general")
const [submitting, setSubmitting] = useState(false)
const [contactMode, setContactMode] = useState<"email" | "phone">("email")
const nameRef = useRef<HTMLInputElement>(null)
@@ -34,7 +46,7 @@ export function IdentityStep({ onSubmit, amount }: Props) {
if (!isValid) return
setSubmitting(true)
try {
await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid })
await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid, fundType: zakatEnabled ? fundType : undefined })
} catch {
setSubmitting(false)
}
@@ -122,6 +134,37 @@ export function IdentityStep({ onSubmit, amount }: Props) {
)}
</div>
{/* Fund Type — only when org has Zakat enabled */}
{zakatEnabled && (
<div className="space-y-2 animate-fade-in">
<p className="text-sm font-bold text-gray-900">
{fundAllocation ? `Fund: ${fundAllocation}` : "What is this donation for?"}
</p>
<div className="grid grid-cols-2 gap-2">
{FUND_TYPES.map((ft) => (
<button
key={ft.id}
onClick={() => setFundType(ft.id)}
className={`text-left rounded-xl border-2 p-3 transition-all ${
fundType === ft.id
? "border-trust-blue bg-trust-blue/5 shadow-sm"
: "border-gray-100 hover:border-gray-200"
}`}
>
<span className="text-lg">{ft.icon}</span>
<p className={`text-xs font-bold mt-1 ${fundType === ft.id ? "text-trust-blue" : "text-gray-900"}`}>{ft.label}</p>
<p className="text-[10px] text-muted-foreground leading-tight">{ft.desc}</p>
</button>
))}
</div>
{fundType === "zakat" && (
<div className="rounded-xl bg-warm-amber/5 border border-warm-amber/20 p-3 text-xs text-muted-foreground animate-fade-in">
Zakat is distributed according to the eight categories specified in the Quran (9:60). This charity is Zakat-eligible.
</div>
)}
</div>
)}
{/* Gift Aid — the hero */}
<button
onClick={() => setGiftAid(!giftAid)}