diff --git a/.pi/infra.md b/.pi/infra.md index 2a9714e..2f66cb3 100644 --- a/.pi/infra.md +++ b/.pi/infra.md @@ -76,3 +76,7 @@ ssh root@157.245.43.50 | `GOOGLE_PLACES_API_KEY` | Google Places autocomplete | | `CT_STRAVA_*` | Strava challenge tracker | | `WORDPRESS_URL`, `WORDPRESS_KEY` | WordPress (Cloudways) | + +## CharityRight n8n +- **URL**: https://n8n.charityright.org.uk +- **API Key**: stored in .env as N8N_CR_API_KEY diff --git a/pledge-now-pay-later/prisma/migrations/20260303_zakat_funds/migration.sql b/pledge-now-pay-later/prisma/migrations/20260303_zakat_funds/migration.sql new file mode 100644 index 0000000..0a69a0e --- /dev/null +++ b/pledge-now-pay-later/prisma/migrations/20260303_zakat_funds/migration.sql @@ -0,0 +1,4 @@ +-- Zakat / fund type tracking +ALTER TABLE "Organization" ADD COLUMN IF NOT EXISTS "zakatEnabled" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "Pledge" ADD COLUMN IF NOT EXISTS "fundType" TEXT; +ALTER TABLE "Event" ADD COLUMN IF NOT EXISTS "fundAllocation" TEXT; diff --git a/pledge-now-pay-later/prisma/schema.prisma b/pledge-now-pay-later/prisma/schema.prisma index 1e479c6..76e3c2f 100644 --- a/pledge-now-pay-later/prisma/schema.prisma +++ b/pledge-now-pay-later/prisma/schema.prisma @@ -24,6 +24,7 @@ model Organization { gcAccessToken String? gcEnvironment String @default("sandbox") whatsappConnected Boolean @default(false) + zakatEnabled Boolean @default(false) // enables Zakat / Sadaqah / Lillah fund type picker createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -62,6 +63,7 @@ model Event { paymentMode String @default("self") // self = we show bank details, external = redirect to URL externalUrl String? // e.g. https://launchgood.com/my-campaign externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other + fundAllocation String? // e.g. "Mosque Building Fund" — tracks which fund this event raises for organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @@ -102,6 +104,7 @@ model Pledge { donorEmail String? donorPhone String? giftAid Boolean @default(false) + fundType String? // null=general, zakat, sadaqah, lillah, fitrana iPaidClickedAt DateTime? notes String? diff --git a/pledge-now-pay-later/src/app/api/events/route.ts b/pledge-now-pay-later/src/app/api/events/route.ts index 081cfa0..d732eef 100644 --- a/pledge-now-pay-later/src/app/api/events/route.ts +++ b/pledge-now-pay-later/src/app/api/events/route.ts @@ -104,6 +104,7 @@ export async function POST(request: NextRequest) { paymentMode: parsed.data.paymentMode || "self", externalUrl: parsed.data.externalUrl, externalPlatform: parsed.data.externalPlatform, + fundAllocation: parsed.data.fundAllocation, slug: slug + "-" + Date.now().toString(36), eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null, organizationId: orgId, diff --git a/pledge-now-pay-later/src/app/api/onboarding/route.ts b/pledge-now-pay-later/src/app/api/onboarding/route.ts index 8c708e4..91be11d 100644 --- a/pledge-now-pay-later/src/app/api/onboarding/route.ts +++ b/pledge-now-pay-later/src/app/api/onboarding/route.ts @@ -45,16 +45,16 @@ export async function GET() { steps.push( { id: "bank", label: "Add bank details", desc: "So donors know where to send money", done: hasBank, href: "/dashboard/settings" }, { id: "whatsapp", label: "Connect WhatsApp", desc: "Auto-send receipts & reminders to donors", done: hasWhatsApp, href: "/dashboard/settings", action: "whatsapp" }, - { id: "event", label: "Create a fundraiser", desc: "Give your campaign a name & goal", done: hasEvent, href: "/dashboard/events" }, - { id: "share", label: "Share your first link", desc: "Generate a QR code or copy the link", done: hasQr, href: "/dashboard/events" }, + { id: "event", label: "Create a campaign", desc: "Name your fundraiser and set a goal", done: hasEvent, href: "/dashboard/events" }, + { id: "share", label: "Share your pledge link", desc: "Send via WhatsApp, social media, email, or print a QR", done: hasQr, href: "/dashboard/events" }, ) } else { // FUNDRAISER flow — needs external URL, no bank steps.push( { id: "event", label: "Add your fundraising page", desc: "Paste your LaunchGood, Enthuse or JustGiving link", done: hasExternalEvent > 0, href: "/dashboard/events" }, { id: "whatsapp", label: "Connect WhatsApp", desc: "Auto-remind donors to complete their pledge", done: hasWhatsApp, href: "/dashboard/settings", action: "whatsapp" }, - { id: "share", label: "Share your pledge link", desc: "Generate a QR or copy the link to share", done: hasQr, href: "/dashboard/events" }, - { id: "pledge", label: "Get your first pledge", desc: "Share with friends & family to start", done: hasPledge, href: "/dashboard/pledges" }, + { id: "share", label: "Share your pledge link", desc: "WhatsApp, social media, email, or QR code", done: hasQr, href: "/dashboard/events" }, + { id: "pledge", label: "Get your first pledge", desc: "Share with your network to start collecting", done: hasPledge, href: "/dashboard/pledges" }, ) } diff --git a/pledge-now-pay-later/src/app/api/pledges/route.ts b/pledge-now-pay-later/src/app/api/pledges/route.ts index b15cb2d..324fadb 100644 --- a/pledge-now-pay-later/src/app/api/pledges/route.ts +++ b/pledge-now-pay-later/src/app/api/pledges/route.ts @@ -105,7 +105,7 @@ export async function POST(request: NextRequest) { ) } - const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data + const { amountPence, rail, donorName, donorEmail, donorPhone, giftAid, fundType, eventId, qrSourceId, scheduleMode, dueDate, installmentCount, installmentDates } = parsed.data // Get event + org const event = await prisma.event.findUnique({ @@ -161,6 +161,7 @@ export async function POST(request: NextRequest) { donorEmail: donorEmail || null, donorPhone: donorPhone || null, giftAid, + fundType: fundType || null, eventId, qrSourceId: qrSourceId || null, organizationId: org.id, @@ -230,6 +231,7 @@ export async function POST(request: NextRequest) { donorEmail: donorEmail || null, donorPhone: donorPhone || null, giftAid, + fundType: fundType || null, eventId, qrSourceId: qrSourceId || null, organizationId: org.id, diff --git a/pledge-now-pay-later/src/app/api/qr/[token]/route.ts b/pledge-now-pay-later/src/app/api/qr/[token]/route.ts index f513eaa..671a11d 100644 --- a/pledge-now-pay-later/src/app/api/qr/[token]/route.ts +++ b/pledge-now-pay-later/src/app/api/qr/[token]/route.ts @@ -16,7 +16,7 @@ export async function GET( if (token === "demo") { const event = await prisma.event.findFirst({ where: { status: "active" }, - include: { organization: { select: { name: true } } }, + include: { organization: { select: { name: true, zakatEnabled: true } } }, orderBy: { createdAt: "asc" }, }) if (!event) { @@ -31,6 +31,8 @@ export async function GET( paymentMode: event.paymentMode || "self", externalUrl: event.externalUrl || null, externalPlatform: event.externalPlatform || null, + zakatEnabled: event.organization.zakatEnabled || false, + fundAllocation: event.fundAllocation || null, }) } @@ -39,7 +41,7 @@ export async function GET( include: { event: { include: { - organization: { select: { name: true } }, + organization: { select: { name: true, zakatEnabled: true } }, }, }, }, @@ -64,6 +66,8 @@ export async function GET( paymentMode: qrSource.event.paymentMode || "self", externalUrl: qrSource.event.externalUrl || null, externalPlatform: qrSource.event.externalPlatform || null, + zakatEnabled: qrSource.event.organization.zakatEnabled || false, + fundAllocation: qrSource.event.fundAllocation || null, }) } catch (error) { console.error("QR resolve error:", error) diff --git a/pledge-now-pay-later/src/app/api/settings/route.ts b/pledge-now-pay-later/src/app/api/settings/route.ts index fc1fed3..d977a14 100644 --- a/pledge-now-pay-later/src/app/api/settings/route.ts +++ b/pledge-now-pay-later/src/app/api/settings/route.ts @@ -25,6 +25,8 @@ export async function GET(request: NextRequest) { primaryColor: org.primaryColor, gcAccessToken: org.gcAccessToken ? "••••••••" : "", gcEnvironment: org.gcEnvironment, + orgType: org.orgType || "charity", + zakatEnabled: org.zakatEnabled || false, }) } catch (error) { console.error("Settings GET error:", error) @@ -80,13 +82,16 @@ export async function PATCH(request: NextRequest) { if (!orgId) return NextResponse.json({ error: "Org not found" }, { status: 404 }) const body = await request.json() - const allowed = ["name", "bankName", "bankSortCode", "bankAccountNo", "bankAccountName", "refPrefix", "primaryColor", "logo", "gcAccessToken", "gcEnvironment"] - const data: Record = {} - for (const key of allowed) { + const stringKeys = ["name", "bankName", "bankSortCode", "bankAccountNo", "bankAccountName", "refPrefix", "primaryColor", "logo", "gcAccessToken", "gcEnvironment"] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: Record = {} + for (const key of stringKeys) { if (key in body && body[key] !== undefined && body[key] !== "••••••••") { data[key] = body[key] } } + // Boolean fields + if ("zakatEnabled" in body) data.zakatEnabled = !!body.zakatEnabled const org = await prisma.organization.update({ where: { id: orgId }, diff --git a/pledge-now-pay-later/src/app/dashboard/events/[id]/page.tsx b/pledge-now-pay-later/src/app/dashboard/events/[id]/page.tsx index 5f0bd72..77ba007 100644 --- a/pledge-now-pay-later/src/app/dashboard/events/[id]/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/events/[id]/page.tsx @@ -8,11 +8,11 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { formatPence } from "@/lib/utils" -import { Plus, Download, QrCode, ExternalLink, Copy, Check, Loader2, ArrowLeft, Trophy, Users, MessageCircle } from "lucide-react" +import { Plus, Download, ExternalLink, Copy, Check, Loader2, ArrowLeft, Trophy, Users, MessageCircle, Mail, Share2, Link2 } from "lucide-react" import Link from "next/link" import { QRCodeCanvas } from "@/components/qr-code" -interface QrSourceInfo { +interface SourceInfo { id: string label: string code: string @@ -23,10 +23,10 @@ interface QrSourceInfo { totalPledged: number } -export default function EventQRPage() { +export default function CampaignLinksPage() { const params = useParams() const eventId = params.id as string - const [qrSources, setQrSources] = useState([]) + const [sources, setSources] = useState([]) const [loading, setLoading] = useState(true) const [showCreate, setShowCreate] = useState(false) const [copiedCode, setCopiedCode] = useState(null) @@ -38,9 +38,7 @@ export default function EventQRPage() { useEffect(() => { fetch(`/api/events/${eventId}/qr`) .then((r) => r.json()) - .then((data) => { - if (Array.isArray(data)) setQrSources(data) - }) + .then((data) => { if (Array.isArray(data)) setSources(data) }) .catch(() => {}) .finally(() => setLoading(false)) }, [eventId]) @@ -48,9 +46,29 @@ export default function EventQRPage() { const copyLink = async (code: string) => { await navigator.clipboard.writeText(`${baseUrl}/p/${code}`) setCopiedCode(code) + if (navigator.vibrate) navigator.vibrate(10) setTimeout(() => setCopiedCode(null), 2000) } + const shareLink = (code: string, label: string) => { + const url = `${baseUrl}/p/${code}` + if (navigator.share) { + navigator.share({ title: label, text: `Pledge here: ${url}`, url }) + } else { + copyLink(code) + } + } + + const shareWhatsApp = (code: string, label: string) => { + const url = `${baseUrl}/p/${code}` + window.open(`https://wa.me/?text=${encodeURIComponent(`Assalamu Alaikum! Please pledge here 🤲\n\n${label}\n${url}`)}`, "_blank") + } + + const shareEmail = (code: string, label: string) => { + const url = `${baseUrl}/p/${code}` + window.open(`mailto:?subject=${encodeURIComponent(`Pledge: ${label}`)}&body=${encodeURIComponent(`Please pledge here:\n\n${url}`)}`) + } + const handleCreate = async () => { setCreating(true) try { @@ -60,8 +78,8 @@ export default function EventQRPage() { body: JSON.stringify(form), }) if (res.ok) { - const qr = await res.json() - setQrSources((prev) => [{ ...qr, scanCount: 0, pledgeCount: 0, totalPledged: 0 }, ...prev]) + const src = await res.json() + setSources((prev) => [{ ...src, scanCount: 0, pledgeCount: 0, totalPledged: 0 }, ...prev]) setShowCreate(false) setForm({ label: "", volunteerName: "", tableName: "" }) } @@ -69,7 +87,6 @@ export default function EventQRPage() { setCreating(false) } - // Auto-generate label useEffect(() => { if (form.volunteerName || form.tableName) { const parts = [form.tableName, form.volunteerName].filter(Boolean) @@ -77,142 +94,118 @@ export default function EventQRPage() { } }, [form.volunteerName, form.tableName]) - if (loading) { - return ( -
- -
- ) - } + if (loading) return
- const totalScans = qrSources.reduce((s, q) => s + q.scanCount, 0) - const totalPledges = qrSources.reduce((s, q) => s + q.pledgeCount, 0) - const totalAmount = qrSources.reduce((s, q) => s + q.totalPledged, 0) + const totalClicks = sources.reduce((s, q) => s + q.scanCount, 0) + const totalPledges = sources.reduce((s, q) => s + q.pledgeCount, 0) + const totalAmount = sources.reduce((s, q) => s + q.totalPledged, 0) return (
- - Back to Events + + Back to Campaigns -

QR Codes

+

Pledge Links

- {qrSources.length} QR code{qrSources.length !== 1 ? "s" : ""} · {totalScans} scans · {totalPledges} pledges · {formatPence(totalAmount)} + {sources.length} link{sources.length !== 1 ? "s" : ""} · {totalClicks} clicks · {totalPledges} pledges · {formatPence(totalAmount)}

- +
- {/* QR Grid */} - {qrSources.length === 0 ? ( + {sources.length === 0 ? ( - -

No QR codes yet. Create one to start collecting pledges!

+ +

Create your first pledge link

+

+ Each link is unique and trackable. Create one per volunteer, table, WhatsApp group, social post, or email campaign — so you know where pledges come from. +

) : (
- {qrSources.map((qr) => ( - + {sources.map((src) => ( + - {/* QR Code */} -
- + {/* QR Code — compact */} +
+
-

{qr.label}

- {qr.volunteerName && ( -

Volunteer: {qr.volunteerName}

- )} +

{src.label}

+ {src.volunteerName &&

By: {src.volunteerName}

} +

{baseUrl}/p/{src.code}

{/* Stats */}
-

{qr.scanCount}

-

Scans

+

{src.scanCount}

+

Clicks

-

{qr.pledgeCount}

+

{src.pledgeCount}

Pledges

-

{formatPence(qr.totalPledged)}

-

Total

+

{formatPence(src.totalPledged)}

+

Raised

- {/* Conversion rate */} - {qr.scanCount > 0 && ( + {src.scanCount > 0 && (
- Conversion: {Math.round((qr.pledgeCount / qr.scanCount) * 100)}% + Conversion: {Math.round((src.pledgeCount / src.scanCount) * 100)}%
)} - {/* Actions */} -
- + + + - - - - - -
- {/* Volunteer & share links */} + + {/* Secondary actions */}
- + - + + + + + +
@@ -223,42 +216,42 @@ export default function EventQRPage() { {/* Create dialog */} - Create QR Code + Create Pledge Link
+

+ Each link is trackable. Create one per source to see where pledges come from. +

+
+ + setForm((f) => ({ ...f, label: e.target.value }))} + /> +
- + setForm((f) => ({ ...f, tableName: e.target.value }))} />
- + setForm((f) => ({ ...f, volunteerName: e.target.value }))} />
-
- - setForm((f) => ({ ...f, label: e.target.value }))} - /> -

Auto-generated from table + volunteer, or enter custom

-
- +
diff --git a/pledge-now-pay-later/src/app/dashboard/events/page.tsx b/pledge-now-pay-later/src/app/dashboard/events/page.tsx index cfecc4b..7cf1ab4 100644 --- a/pledge-now-pay-later/src/app/dashboard/events/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/events/page.tsx @@ -53,7 +53,7 @@ export default function EventsPage() { }, []) const [creating, setCreating] = useState(false) const [orgType, setOrgType] = useState(null) - const [form, setForm] = useState({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: "self" as "self" | "external", externalUrl: "", externalPlatform: "" }) + const [form, setForm] = useState({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: "self" as "self" | "external", externalUrl: "", externalPlatform: "", fundAllocation: "" }) // Fetch org type to customize the form useEffect(() => { @@ -77,15 +77,16 @@ export default function EventsPage() { goalAmount: form.goalAmount ? Math.round(parseFloat(form.goalAmount) * 100) : undefined, eventDate: form.eventDate ? new Date(form.eventDate).toISOString() : undefined, paymentMode: form.paymentMode, - externalUrl: form.paymentMode === "external" ? form.externalUrl : undefined, + externalUrl: form.externalUrl || undefined, externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined, + fundAllocation: form.fundAllocation || undefined, }), }) if (res.ok) { const event = await res.json() setEvents((prev) => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev]) setShowCreate(false) - setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "" }) + setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "", paymentMode: orgType === "fundraiser" ? "external" : "self", externalUrl: "", externalPlatform: "", fundAllocation: "" }) } } catch { // handle error @@ -97,11 +98,11 @@ export default function EventsPage() {
-

Events

-

Manage your fundraising events and QR codes

+

Campaigns

+

Create campaigns, share pledge links, and track donations

@@ -177,7 +178,7 @@ export default function EventsPage() {
@@ -195,13 +196,13 @@ export default function EventsPage() { {/* Create dialog */} - Create Event + New Campaign
- + setForm((f) => ({ ...f, name: e.target.value }))} /> @@ -297,12 +298,38 @@ export default function EventsPage() {
)} + {/* Fund allocation — for charities tracking which fund this goes to */} + {form.paymentMode === "self" && ( +
+ + setForm(f => ({ ...f, fundAllocation: e.target.value }))} + /> +

Track which fund this event raises for. Shows on pledge receipts & reports.

+
+ +
+ + setForm(f => ({ ...f, externalUrl: e.target.value }))} + className="pl-9" + /> +
+

Link an external campaign page for reference & allocation tracking.

+
+
+ )} +
diff --git a/pledge-now-pay-later/src/app/dashboard/layout.tsx b/pledge-now-pay-later/src/app/dashboard/layout.tsx index 7aebb10..ee88afd 100644 --- a/pledge-now-pay-later/src/app/dashboard/layout.tsx +++ b/pledge-now-pay-later/src/app/dashboard/layout.tsx @@ -3,12 +3,12 @@ import Link from "next/link" import { usePathname } from "next/navigation" import { useSession, signOut } from "next-auth/react" -import { LayoutDashboard, Calendar, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut, Shield } from "lucide-react" +import { LayoutDashboard, Megaphone, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut, Shield } from "lucide-react" import { cn } from "@/lib/utils" const navItems = [ { href: "/dashboard", label: "Overview", icon: LayoutDashboard }, - { href: "/dashboard/events", label: "Events", icon: Calendar }, + { href: "/dashboard/events", label: "Campaigns", icon: Megaphone }, { href: "/dashboard/pledges", label: "Pledges", icon: FileBarChart }, { href: "/dashboard/reconcile", label: "Reconcile", icon: Upload }, { href: "/dashboard/exports", label: "Exports", icon: Download }, @@ -39,7 +39,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
diff --git a/pledge-now-pay-later/src/app/dashboard/page.tsx b/pledge-now-pay-later/src/app/dashboard/page.tsx index b2c360f..07e6815 100644 --- a/pledge-now-pay-later/src/app/dashboard/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/page.tsx @@ -302,7 +302,7 @@ export default function DashboardPage() {

- +
diff --git a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx index 5f919d9..2ff6c35 100644 --- a/pledge-now-pay-later/src/app/dashboard/settings/page.tsx +++ b/pledge-now-pay-later/src/app/dashboard/settings/page.tsx @@ -21,6 +21,8 @@ interface OrgSettings { primaryColor: string gcAccessToken: string gcEnvironment: string + orgType: string + zakatEnabled: boolean } export default function SettingsPage() { @@ -116,6 +118,42 @@ export default function SettingsPage() { + {/* Zakat & Fund Types */} + + + ☪️ Zakat & Fund Types + Let donors specify their donation type (Zakat, Sadaqah, Lillah, Fitrana) + + + + {settings.zakatEnabled && ( +
+

🌙 Zakat — Obligatory 2.5% annual charity

+

🤲 Sadaqah / General — Voluntary donations

+

🌱 Sadaqah Jariyah — Ongoing charity (buildings, wells)

+

🕌 Lillah — For the mosque / institution

+

🍽️ Fitrana — Zakat al-Fitr (before Eid)

+
+ )} +
+
+ {/* Branding */} diff --git a/pledge-now-pay-later/src/app/p/[token]/page.tsx b/pledge-now-pay-later/src/app/p/[token]/page.tsx index 99f5a1d..6287d1f 100644 --- a/pledge-now-pay-later/src/app/p/[token]/page.tsx +++ b/pledge-now-pay-later/src/app/p/[token]/page.tsx @@ -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: , 1: , 2: , - 3: , + 3: , 4: pledgeResult && , 5: pledgeResult && ( 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(fundAllocation ? "general" : "general") const [submitting, setSubmitting] = useState(false) const [contactMode, setContactMode] = useState<"email" | "phone">("email") const nameRef = useRef(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) { )}
+ {/* Fund Type — only when org has Zakat enabled */} + {zakatEnabled && ( +
+

+ {fundAllocation ? `Fund: ${fundAllocation}` : "What is this donation for?"} +

+
+ {FUND_TYPES.map((ft) => ( + + ))} +
+ {fundType === "zakat" && ( +
+ ☪️ Zakat is distributed according to the eight categories specified in the Quran (9:60). This charity is Zakat-eligible. +
+ )} +
+ )} + {/* Gift Aid — the hero */}
- - Sign In - - - Get Started Free - + Sign In + Get Started Free
@@ -27,15 +23,14 @@ export default function HomePage() {
- 🇬🇧 Built for UK charities · Free to start + 🇬🇧 Built for UK charities & fundraisers · Free forever
-

- Turn promises into - payments +

+ Collect pledges.
+ Convert them into donations.

-

- At your next event, donors pledge what they want to give — then pay on their own terms. - You get QR codes, WhatsApp reminders, and a dashboard to track every penny. +

+ Someone says "I'll donate £5,000" — then what? PNPL captures that promise, sends WhatsApp reminders, and tracks every penny until it lands. At events, on social media, in WhatsApp groups, or one-on-one with high-net-worth donors.

@@ -49,50 +44,130 @@ export default function HomePage() {
- {/* Problem */} -
-
+ {/* Use Cases — the 4 audiences */} +
+
+
+

Who uses PNPL?

+

Anyone who collects promises of money and needs them fulfilled.

+
+
+ {/* Charity at events */} +
+
+
🕌
+
+

Charities & Mosques at Events

+

Gala dinners, Ramadan nights, Jumuah appeals

+
+
+

Print QR codes for each table. Donors scan, pledge an amount, and choose to pay now or later. You get a dashboard with every pledge, automatic WhatsApp reminders, and bank reconciliation.

+
+ QR per table + Bank transfer + Gift Aid + Zakat tracking +
+
+ + {/* HNW donors */} +
+
+
💎
+
+

High-Net-Worth Donor Outreach

+

Major gifts, personal pledges, board commitments

+
+
+

Send a personal pledge link to a major donor via WhatsApp or email. They commit to £50k over 6 months. PNPL tracks each instalment, sends reminders before due dates, and shows you the full pipeline.

+
+ Personal links + Instalments + WhatsApp follow-up +
+
+ + {/* Org-to-org */} +
+
+
🏗️
+
+

Org-to-Org Pledges

+

Multi-charity projects, umbrella fundraising

+
+
+

Coordinating a large project across multiple charities? Each org pledges their share. Track commitments from organisations, not just individuals. Allocate funds to specific projects and see who's delivered.

+
+ Fund allocation + Multi-party + Project tracking +
+
+ + {/* Personal fundraiser */} +
+
+
❤️
+
+

Personal Fundraisers

+

LaunchGood, Enthuse, JustGiving, GoFundMe

+
+
+

Already have a fundraising page? Share your PNPL link on WhatsApp and social media. People pledge an amount, then get redirected to your LaunchGood/Enthuse/JustGiving page to pay. You see who actually followed through.

+
+ External redirect + Social sharing + Conversion tracking +
+
+
+
+
+ + {/* The Problem */} +
+
+
+

The Problem

+

30-50% of pledges never convert

+
😤
-

Pledges go cold

-

Donors say "I'll pay £500" at the gala, then forget. You have no way to follow up.

-
-
-
📝
-

Paper tracking

-

Spreadsheets, napkin notes, WhatsApp groups. No system, no references, no proof.

+

No follow-up system

+

Someone pledges £5,000 at your dinner. You write it on a napkin. A week later — who pledged what? No idea.

💸

Money left on the table

-

UK charities lose 30-50% of pledged amounts because there's no follow-up system.

+

Donors meant it when they said it. But without a reminder and easy payment path, life gets in the way.

+
+
+
🕌
+

Funds mixed up

+

Zakat mixed with Sadaqah. Building fund mixed with general. No audit trail for fund allocation.

{/* How it works */} -
+

How it works

-

From pledge to payment in 4 steps

+

Whether it's a QR code at a gala or a link in a WhatsApp group

{[ - { step: "1", icon: "📱", title: "Donor scans QR", desc: "At your event, each table/volunteer has a unique QR code." }, - { step: "2", icon: "🤲", title: "Pledges amount", desc: "Pick an amount. Choose to pay now, on a date, or monthly instalments." }, - { step: "3", icon: "💬", title: "Gets reminders", desc: "WhatsApp messages with bank details before each due date. They reply PAID when done." }, - { step: "4", icon: "✅", title: "You reconcile", desc: "Dashboard shows who pledged, who paid, who needs a nudge. Upload bank statements to auto-match." }, + { step: "1", icon: "🔗", title: "Share a pledge link", desc: "Create a trackable link. Share via QR code, WhatsApp, social media, email, or send directly to a major donor." }, + { step: "2", icon: "🤲", title: "Donor pledges", desc: "They pick an amount, choose Zakat/Sadaqah, and decide: pay now, on a date, or in monthly instalments." }, + { step: "3", icon: "💬", title: "WhatsApp follows up", desc: "Automated reminders with bank details or a link to your fundraising page. They reply PAID when done." }, + { step: "4", icon: "📊", title: "You see everything", desc: "Live dashboard: who pledged, who paid, what fund, which source. Export for HMRC Gift Aid." }, ].map((s) => (
-
- {s.icon} -
-
- {s.step} -
+
{s.icon}
+
{s.step}

{s.title}

{s.desc}

@@ -101,25 +176,101 @@ export default function HomePage() {
+ {/* Sharing channels */} +
+
+

Share anywhere. Track everything.

+

Every link is unique and trackable. See exactly where each pledge came from.

+
+ {[ + { icon: "💬", label: "WhatsApp" }, + { icon: "📱", label: "QR Code" }, + { icon: "📧", label: "Email" }, + { icon: "📸", label: "Instagram" }, + { icon: "🐦", label: "Twitter/X" }, + { icon: "👤", label: "1-on-1" }, + ].map((c) => ( +
+ {c.icon} +

{c.label}

+
+ ))} +
+
+
+ + {/* Platforms */} +
+
+

Works with your payment platform

+

Process donations directly, or redirect donors to your existing page.

+
+ {[ + { name: "Bank Transfer (UK)", icon: "🏦", color: "#1e40af" }, + { name: "LaunchGood", icon: "🌙", color: "#00C389" }, + { name: "Enthuse", icon: "💜", color: "#6B4FBB" }, + { name: "JustGiving", icon: "💛", color: "#AD29B6" }, + { name: "GoFundMe", icon: "💚", color: "#00B964" }, + { name: "Any URL", icon: "🔗", color: "#6b7280" }, + ].map((p) => ( +
+ {p.icon} + {p.name} +
+ ))} +
+
+
+ + {/* Fund Types */} +
+
+
+

Islamic fund types built-in

+

Donors choose their fund type. You get clean reporting. Zakat never mixes with Sadaqah.

+
+
+ {[ + { name: "Zakat", icon: "🌙", desc: "Obligatory 2.5%" }, + { name: "Sadaqah", icon: "🤲", desc: "Voluntary" }, + { name: "Sadaqah Jariyah", icon: "🌱", desc: "Ongoing" }, + { name: "Lillah", icon: "🕌", desc: "For institution" }, + { name: "Fitrana", icon: "🍽️", desc: "Before Eid" }, + ].map((f) => ( +
+ {f.icon} +

{f.name}

+

{f.desc}

+
+ ))} +
+

+ Enable in Settings → Fund Types. Donors see the picker during pledge. Reports break down by type. +

+
+
+ {/* Features */} -
+

Everything you need

{[ - { icon: "📱", title: "QR Code Generator", desc: "Unique codes per volunteer/table. Track who brings in the most." }, - { icon: "📅", title: "Flexible Scheduling", desc: "Pay now, pick a date, or split into 2-12 monthly instalments." }, - { icon: "💬", title: "WhatsApp Reminders", desc: "Auto-send bank details and reminders. Donors reply PAID, HELP, or CANCEL." }, - { icon: "🎁", title: "Gift Aid", desc: "Collect declarations inline. Export HMRC-ready CSV with one click." }, - { icon: "🏦", title: "UK Bank Transfers", desc: "Unique reference per pledge for easy reconciliation. Tap-to-copy details." }, - { icon: "📊", title: "Live Dashboard", desc: "See pledges come in real-time. Pipeline view: pending → initiated → paid." }, - { icon: "🏆", title: "Volunteer Leaderboard", desc: "Real-time scoreboard. Motivate your team with friendly competition." }, - { icon: "📤", title: "CRM Export", desc: "Download all pledge data as CSV. Gift Aid pack for HMRC." }, + { icon: "🔗", title: "Trackable Pledge Links", desc: "Create unique links per source — WhatsApp group, social post, email, volunteer, table. See where pledges come from." }, + { icon: "📅", title: "Flexible Scheduling", desc: "Pay now, pick a date, or split into 2-12 monthly instalments. Each instalment tracked separately." }, + { icon: "💬", title: "WhatsApp Reminders", desc: "Automated multi-step: 2 days before → due day → gentle nudge → final. Donors reply PAID, HELP, STATUS." }, + { icon: "🎁", title: "Gift Aid + HMRC Export", desc: "Collect declarations inline with live math. One-click HMRC-ready CSV export." }, + { icon: "☪️", title: "Fund Type Tracking", desc: "Zakat, Sadaqah, Lillah, Fitrana. Clean fund-level reporting. Optional — enable when you need it." }, + { icon: "📊", title: "Live Dashboard", desc: "Real-time pipeline: new → initiated → paid → overdue. Needs-attention alerts. Auto-refreshes." }, + { icon: "🏦", title: "Fund Allocation", desc: "Link campaigns to specific funds. Charities can also link external fundraising pages for allocation tracking." }, + { icon: "🏆", title: "Leaderboard", desc: "See which volunteer, table, or link source brings in the most pledges. Friendly competition." }, + { icon: "📱", title: "QR Codes for Events", desc: "Print a QR code per table. Works alongside WhatsApp sharing, social posts, and direct links." }, + { icon: "📤", title: "CRM Export", desc: "Download all pledge data as CSV. Filter by fund type, campaign, status, or source." }, ].map((f) => ( -
- {f.icon} +
+ {f.icon}

{f.title}

{f.desc}

@@ -131,24 +282,24 @@ export default function HomePage() {
{/* Donor schedule */} -
+

Donors choose when to pay

-
+

Pay Now

-

Card, bank transfer, or Direct Debit right away

+

Bank transfer or redirect to your fundraising page

-
+
📅

Pick a Date

-

"I'll pay on payday" — reminders sent automatically

+

"I'll pay on payday" — WhatsApp reminders sent automatically

-
+
📆

Monthly

-

Split into 2-12 instalments. Each one tracked separately

+

Split into 2-12 instalments. Each one tracked & reminded

@@ -157,14 +308,18 @@ export default function HomePage() { {/* CTA */}
-

Start collecting pledges today

-

- Free to use. Set up in 2 minutes. No technical knowledge needed. +

Stop losing pledges.

+

+ Free to use. Set up in 2 minutes. Whether you're collecting pledges at a gala dinner, from your WhatsApp contacts, or from other organisations for a joint project.

- - Create Your Free Account → - -

Used by mosques, churches, schools and charities across the UK

+
+ + Create Your Free Account → + + + 🎮 Try the Demo + +
@@ -172,14 +327,13 @@ export default function HomePage() {