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

@@ -76,3 +76,7 @@ ssh root@157.245.43.50
| `GOOGLE_PLACES_API_KEY` | Google Places autocomplete | | `GOOGLE_PLACES_API_KEY` | Google Places autocomplete |
| `CT_STRAVA_*` | Strava challenge tracker | | `CT_STRAVA_*` | Strava challenge tracker |
| `WORDPRESS_URL`, `WORDPRESS_KEY` | WordPress (Cloudways) | | `WORDPRESS_URL`, `WORDPRESS_KEY` | WordPress (Cloudways) |
## CharityRight n8n
- **URL**: https://n8n.charityright.org.uk
- **API Key**: stored in .env as N8N_CR_API_KEY

View File

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

View File

@@ -24,6 +24,7 @@ model Organization {
gcAccessToken String? gcAccessToken String?
gcEnvironment String @default("sandbox") gcEnvironment String @default("sandbox")
whatsappConnected Boolean @default(false) whatsappConnected Boolean @default(false)
zakatEnabled Boolean @default(false) // enables Zakat / Sadaqah / Lillah fund type picker
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -62,6 +63,7 @@ model Event {
paymentMode String @default("self") // self = we show bank details, external = redirect to URL paymentMode String @default("self") // self = we show bank details, external = redirect to URL
externalUrl String? // e.g. https://launchgood.com/my-campaign externalUrl String? // e.g. https://launchgood.com/my-campaign
externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other
fundAllocation String? // e.g. "Mosque Building Fund" — tracks which fund this event raises for
organizationId String organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -102,6 +104,7 @@ model Pledge {
donorEmail String? donorEmail String?
donorPhone String? donorPhone String?
giftAid Boolean @default(false) giftAid Boolean @default(false)
fundType String? // null=general, zakat, sadaqah, lillah, fitrana
iPaidClickedAt DateTime? iPaidClickedAt DateTime?
notes String? notes String?

View File

@@ -104,6 +104,7 @@ export async function POST(request: NextRequest) {
paymentMode: parsed.data.paymentMode || "self", paymentMode: parsed.data.paymentMode || "self",
externalUrl: parsed.data.externalUrl, externalUrl: parsed.data.externalUrl,
externalPlatform: parsed.data.externalPlatform, externalPlatform: parsed.data.externalPlatform,
fundAllocation: parsed.data.fundAllocation,
slug: slug + "-" + Date.now().toString(36), slug: slug + "-" + Date.now().toString(36),
eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null, eventDate: parsed.data.eventDate ? new Date(parsed.data.eventDate) : null,
organizationId: orgId, organizationId: orgId,

View File

@@ -45,16 +45,16 @@ export async function GET() {
steps.push( steps.push(
{ id: "bank", label: "Add bank details", desc: "So donors know where to send money", done: hasBank, href: "/dashboard/settings" }, { 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: "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: "event", label: "Create a campaign", desc: "Name your fundraiser and set a 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: "share", label: "Share your pledge link", desc: "Send via WhatsApp, social media, email, or print a QR", done: hasQr, href: "/dashboard/events" },
) )
} else { } else {
// FUNDRAISER flow — needs external URL, no bank // FUNDRAISER flow — needs external URL, no bank
steps.push( steps.push(
{ id: "event", label: "Add your fundraising page", desc: "Paste your LaunchGood, Enthuse or JustGiving link", done: hasExternalEvent > 0, href: "/dashboard/events" }, { 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: "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: "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 friends & family to start", done: hasPledge, href: "/dashboard/pledges" }, { id: "pledge", label: "Get your first pledge", desc: "Share with your network to start collecting", done: hasPledge, href: "/dashboard/pledges" },
) )
} }

View File

@@ -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 // Get event + org
const event = await prisma.event.findUnique({ const event = await prisma.event.findUnique({
@@ -161,6 +161,7 @@ export async function POST(request: NextRequest) {
donorEmail: donorEmail || null, donorEmail: donorEmail || null,
donorPhone: donorPhone || null, donorPhone: donorPhone || null,
giftAid, giftAid,
fundType: fundType || null,
eventId, eventId,
qrSourceId: qrSourceId || null, qrSourceId: qrSourceId || null,
organizationId: org.id, organizationId: org.id,
@@ -230,6 +231,7 @@ export async function POST(request: NextRequest) {
donorEmail: donorEmail || null, donorEmail: donorEmail || null,
donorPhone: donorPhone || null, donorPhone: donorPhone || null,
giftAid, giftAid,
fundType: fundType || null,
eventId, eventId,
qrSourceId: qrSourceId || null, qrSourceId: qrSourceId || null,
organizationId: org.id, organizationId: org.id,

View File

@@ -16,7 +16,7 @@ export async function GET(
if (token === "demo") { if (token === "demo") {
const event = await prisma.event.findFirst({ const event = await prisma.event.findFirst({
where: { status: "active" }, where: { status: "active" },
include: { organization: { select: { name: true } } }, include: { organization: { select: { name: true, zakatEnabled: true } } },
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
}) })
if (!event) { if (!event) {
@@ -31,6 +31,8 @@ export async function GET(
paymentMode: event.paymentMode || "self", paymentMode: event.paymentMode || "self",
externalUrl: event.externalUrl || null, externalUrl: event.externalUrl || null,
externalPlatform: event.externalPlatform || null, externalPlatform: event.externalPlatform || null,
zakatEnabled: event.organization.zakatEnabled || false,
fundAllocation: event.fundAllocation || null,
}) })
} }
@@ -39,7 +41,7 @@ export async function GET(
include: { include: {
event: { event: {
include: { include: {
organization: { select: { name: true } }, organization: { select: { name: true, zakatEnabled: true } },
}, },
}, },
}, },
@@ -64,6 +66,8 @@ export async function GET(
paymentMode: qrSource.event.paymentMode || "self", paymentMode: qrSource.event.paymentMode || "self",
externalUrl: qrSource.event.externalUrl || null, externalUrl: qrSource.event.externalUrl || null,
externalPlatform: qrSource.event.externalPlatform || null, externalPlatform: qrSource.event.externalPlatform || null,
zakatEnabled: qrSource.event.organization.zakatEnabled || false,
fundAllocation: qrSource.event.fundAllocation || null,
}) })
} catch (error) { } catch (error) {
console.error("QR resolve error:", error) console.error("QR resolve error:", error)

View File

@@ -25,6 +25,8 @@ export async function GET(request: NextRequest) {
primaryColor: org.primaryColor, primaryColor: org.primaryColor,
gcAccessToken: org.gcAccessToken ? "••••••••" : "", gcAccessToken: org.gcAccessToken ? "••••••••" : "",
gcEnvironment: org.gcEnvironment, gcEnvironment: org.gcEnvironment,
orgType: org.orgType || "charity",
zakatEnabled: org.zakatEnabled || false,
}) })
} catch (error) { } catch (error) {
console.error("Settings GET error:", 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 }) if (!orgId) return NextResponse.json({ error: "Org not found" }, { status: 404 })
const body = await request.json() const body = await request.json()
const allowed = ["name", "bankName", "bankSortCode", "bankAccountNo", "bankAccountName", "refPrefix", "primaryColor", "logo", "gcAccessToken", "gcEnvironment"] const stringKeys = ["name", "bankName", "bankSortCode", "bankAccountNo", "bankAccountName", "refPrefix", "primaryColor", "logo", "gcAccessToken", "gcEnvironment"]
const data: Record<string, string> = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const key of allowed) { const data: Record<string, any> = {}
for (const key of stringKeys) {
if (key in body && body[key] !== undefined && body[key] !== "••••••••") { if (key in body && body[key] !== undefined && body[key] !== "••••••••") {
data[key] = body[key] data[key] = body[key]
} }
} }
// Boolean fields
if ("zakatEnabled" in body) data.zakatEnabled = !!body.zakatEnabled
const org = await prisma.organization.update({ const org = await prisma.organization.update({
where: { id: orgId }, where: { id: orgId },

View File

@@ -8,11 +8,11 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { formatPence } from "@/lib/utils" 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 Link from "next/link"
import { QRCodeCanvas } from "@/components/qr-code" import { QRCodeCanvas } from "@/components/qr-code"
interface QrSourceInfo { interface SourceInfo {
id: string id: string
label: string label: string
code: string code: string
@@ -23,10 +23,10 @@ interface QrSourceInfo {
totalPledged: number totalPledged: number
} }
export default function EventQRPage() { export default function CampaignLinksPage() {
const params = useParams() const params = useParams()
const eventId = params.id as string const eventId = params.id as string
const [qrSources, setQrSources] = useState<QrSourceInfo[]>([]) const [sources, setSources] = useState<SourceInfo[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [copiedCode, setCopiedCode] = useState<string | null>(null) const [copiedCode, setCopiedCode] = useState<string | null>(null)
@@ -38,9 +38,7 @@ export default function EventQRPage() {
useEffect(() => { useEffect(() => {
fetch(`/api/events/${eventId}/qr`) fetch(`/api/events/${eventId}/qr`)
.then((r) => r.json()) .then((r) => r.json())
.then((data) => { .then((data) => { if (Array.isArray(data)) setSources(data) })
if (Array.isArray(data)) setQrSources(data)
})
.catch(() => {}) .catch(() => {})
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, [eventId]) }, [eventId])
@@ -48,9 +46,29 @@ export default function EventQRPage() {
const copyLink = async (code: string) => { const copyLink = async (code: string) => {
await navigator.clipboard.writeText(`${baseUrl}/p/${code}`) await navigator.clipboard.writeText(`${baseUrl}/p/${code}`)
setCopiedCode(code) setCopiedCode(code)
if (navigator.vibrate) navigator.vibrate(10)
setTimeout(() => setCopiedCode(null), 2000) 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 () => { const handleCreate = async () => {
setCreating(true) setCreating(true)
try { try {
@@ -60,8 +78,8 @@ export default function EventQRPage() {
body: JSON.stringify(form), body: JSON.stringify(form),
}) })
if (res.ok) { if (res.ok) {
const qr = await res.json() const src = await res.json()
setQrSources((prev) => [{ ...qr, scanCount: 0, pledgeCount: 0, totalPledged: 0 }, ...prev]) setSources((prev) => [{ ...src, scanCount: 0, pledgeCount: 0, totalPledged: 0 }, ...prev])
setShowCreate(false) setShowCreate(false)
setForm({ label: "", volunteerName: "", tableName: "" }) setForm({ label: "", volunteerName: "", tableName: "" })
} }
@@ -69,7 +87,6 @@ export default function EventQRPage() {
setCreating(false) setCreating(false)
} }
// Auto-generate label
useEffect(() => { useEffect(() => {
if (form.volunteerName || form.tableName) { if (form.volunteerName || form.tableName) {
const parts = [form.tableName, form.volunteerName].filter(Boolean) const parts = [form.tableName, form.volunteerName].filter(Boolean)
@@ -77,142 +94,118 @@ export default function EventQRPage() {
} }
}, [form.volunteerName, form.tableName]) }, [form.volunteerName, form.tableName])
if (loading) { if (loading) return <div className="flex items-center justify-center py-20"><Loader2 className="h-8 w-8 text-trust-blue animate-spin" /></div>
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-8 w-8 text-trust-blue animate-spin" />
</div>
)
}
const totalScans = qrSources.reduce((s, q) => s + q.scanCount, 0) const totalClicks = sources.reduce((s, q) => s + q.scanCount, 0)
const totalPledges = qrSources.reduce((s, q) => s + q.pledgeCount, 0) const totalPledges = sources.reduce((s, q) => s + q.pledgeCount, 0)
const totalAmount = qrSources.reduce((s, q) => s + q.totalPledged, 0) const totalAmount = sources.reduce((s, q) => s + q.totalPledged, 0)
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Link <Link href="/dashboard/events" className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 mb-2">
href="/dashboard/events" <ArrowLeft className="h-3 w-3" /> Back to Campaigns
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 mb-2"
>
<ArrowLeft className="h-3 w-3" /> Back to Events
</Link> </Link>
<h1 className="text-3xl font-extrabold text-gray-900">QR Codes</h1> <h1 className="text-3xl font-extrabold text-gray-900">Pledge Links</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
{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)}
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Link href={`/dashboard/events/${eventId}/leaderboard`}> <Link href={`/dashboard/events/${eventId}/leaderboard`}>
<Button variant="outline"> <Button variant="outline"><Trophy className="h-4 w-4 mr-2" /> Leaderboard</Button>
<Trophy className="h-4 w-4 mr-2" /> Leaderboard
</Button>
</Link> </Link>
<Button onClick={() => setShowCreate(true)}> <Button onClick={() => setShowCreate(true)}>
<Plus className="h-4 w-4 mr-2" /> New QR Code <Plus className="h-4 w-4 mr-2" /> New Link
</Button> </Button>
</div> </div>
</div> </div>
{/* QR Grid */} {sources.length === 0 ? (
{qrSources.length === 0 ? (
<Card> <Card>
<CardContent className="py-12 text-center space-y-4"> <CardContent className="py-12 text-center space-y-4">
<QrCode className="h-12 w-12 text-muted-foreground mx-auto" /> <Link2 className="h-12 w-12 text-muted-foreground mx-auto" />
<p className="text-muted-foreground">No QR codes yet. Create one to start collecting pledges!</p> <h3 className="font-bold text-lg">Create your first pledge link</h3>
<p className="text-muted-foreground text-sm max-w-md mx-auto">
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.
</p>
<Button onClick={() => setShowCreate(true)}> <Button onClick={() => setShowCreate(true)}>
<Plus className="h-4 w-4 mr-2" /> Create First QR Code <Plus className="h-4 w-4 mr-2" /> Create Pledge Link
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{qrSources.map((qr) => ( {sources.map((src) => (
<Card key={qr.id} className="hover:shadow-md transition-shadow"> <Card key={src.id} className="hover:shadow-md transition-shadow">
<CardContent className="pt-6 space-y-4"> <CardContent className="pt-6 space-y-4">
{/* QR Code */} {/* QR Code — compact */}
<div className="max-w-[180px] mx-auto bg-white rounded-2xl flex items-center justify-center p-2"> <div className="max-w-[140px] mx-auto bg-white rounded-xl flex items-center justify-center p-1.5">
<QRCodeCanvas url={`${baseUrl}/p/${qr.code}`} size={164} /> <QRCodeCanvas url={`${baseUrl}/p/${src.code}`} size={128} />
</div> </div>
<div className="text-center"> <div className="text-center">
<h3 className="font-bold">{qr.label}</h3> <h3 className="font-bold">{src.label}</h3>
{qr.volunteerName && ( {src.volunteerName && <p className="text-xs text-muted-foreground">By: {src.volunteerName}</p>}
<p className="text-xs text-muted-foreground">Volunteer: {qr.volunteerName}</p> <p className="text-[10px] text-muted-foreground font-mono mt-1">{baseUrl}/p/{src.code}</p>
)}
</div> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-3 gap-2 text-center text-xs"> <div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-lg bg-gray-50 p-2"> <div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{qr.scanCount}</p> <p className="font-bold text-sm">{src.scanCount}</p>
<p className="text-muted-foreground">Scans</p> <p className="text-muted-foreground">Clicks</p>
</div> </div>
<div className="rounded-lg bg-gray-50 p-2"> <div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{qr.pledgeCount}</p> <p className="font-bold text-sm">{src.pledgeCount}</p>
<p className="text-muted-foreground">Pledges</p> <p className="text-muted-foreground">Pledges</p>
</div> </div>
<div className="rounded-lg bg-gray-50 p-2"> <div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{formatPence(qr.totalPledged)}</p> <p className="font-bold text-sm">{formatPence(src.totalPledged)}</p>
<p className="text-muted-foreground">Total</p> <p className="text-muted-foreground">Raised</p>
</div> </div>
</div> </div>
{/* Conversion rate */} {src.scanCount > 0 && (
{qr.scanCount > 0 && (
<div className="text-center"> <div className="text-center">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Conversion: <span className="font-semibold text-foreground">{Math.round((qr.pledgeCount / qr.scanCount) * 100)}%</span> Conversion: <span className="font-semibold text-foreground">{Math.round((src.pledgeCount / src.scanCount) * 100)}%</span>
</span> </span>
</div> </div>
)} )}
{/* Actions */} {/* Share options — the main CTA, not an afterthought */}
<div className="flex gap-2"> <div className="grid grid-cols-4 gap-1.5">
<Button <Button variant="outline" size="sm" onClick={() => copyLink(src.code)} className="text-[10px] px-1">
variant="outline" {copiedCode === src.code ? <Check className="h-3.5 w-3.5" /> : <><Copy className="h-3 w-3" /> Copy</>}
size="sm" </Button>
className="flex-1" <Button size="sm" onClick={() => shareWhatsApp(src.code, src.label)} className="text-[10px] px-1 bg-[#25D366] hover:bg-[#20BD5A] text-white">
onClick={() => copyLink(qr.code)} <MessageCircle className="h-3 w-3" /> WA
> </Button>
{copiedCode === qr.code ? ( <Button variant="outline" size="sm" onClick={() => shareEmail(src.code, src.label)} className="text-[10px] px-1">
<><Check className="h-3 w-3 mr-1" /> Copied</> <Mail className="h-3 w-3" /> Email
) : ( </Button>
<><Copy className="h-3 w-3 mr-1" /> Link</> <Button variant="outline" size="sm" onClick={() => shareLink(src.code, src.label)} className="text-[10px] px-1">
)} <Share2 className="h-3 w-3" /> More
</Button> </Button>
<a href={`/api/events/${eventId}/qr/${qr.id}/download?code=${qr.code}`} download>
<Button variant="outline" size="sm">
<Download className="h-3 w-3 mr-1" /> PNG
</Button>
</a>
<a href={`/p/${qr.code}`} target="_blank">
<Button variant="outline" size="sm">
<ExternalLink className="h-3 w-3" />
</Button>
</a>
</div> </div>
{/* Volunteer & share links */}
{/* Secondary actions */}
<div className="flex gap-2"> <div className="flex gap-2">
<a href={`/v/${qr.code}`} target="_blank" className="flex-1"> <a href={`/api/events/${eventId}/qr/${src.id}/download?code=${src.code}`} download className="flex-1">
<Button variant="outline" size="sm" className="w-full text-xs"> <Button variant="outline" size="sm" className="w-full text-xs">
<Users className="h-3 w-3 mr-1" /> Volunteer View <Download className="h-3 w-3 mr-1" /> QR Image
</Button> </Button>
</a> </a>
<Button <a href={`/v/${src.code}`} target="_blank" className="flex-1">
variant="outline" <Button variant="outline" size="sm" className="w-full text-xs">
size="sm" <Users className="h-3 w-3 mr-1" /> Volunteer
className="flex-1 text-xs bg-[#25D366]/5 border-[#25D366]/30 text-[#25D366] hover:bg-[#25D366]/10" </Button>
onClick={() => { </a>
const url = `${baseUrl}/p/${qr.code}` <a href={`/p/${src.code}`} target="_blank">
const text = `Hi! Scan this to pledge: ${url}` <Button variant="outline" size="sm"><ExternalLink className="h-3 w-3" /></Button>
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank") </a>
}}
>
<MessageCircle className="h-3 w-3 mr-1" /> Share
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -223,42 +216,42 @@ export default function EventQRPage() {
{/* Create dialog */} {/* Create dialog */}
<Dialog open={showCreate} onOpenChange={setShowCreate}> <Dialog open={showCreate} onOpenChange={setShowCreate}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create QR Code</DialogTitle> <DialogTitle>Create Pledge Link</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground">
Each link is trackable. Create one per source to see where pledges come from.
</p>
<div className="space-y-2">
<Label>Label *</Label>
<Input
placeholder="e.g. WhatsApp Family Group, Table 5, Instagram Bio, Ahmed's Link"
value={form.label}
onChange={(e) => setForm((f) => ({ ...f, label: e.target.value }))}
/>
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Table Name</Label> <Label>Source / Channel <span className="text-muted-foreground font-normal">(optional)</span></Label>
<Input <Input
placeholder="e.g. Table 5" placeholder="e.g. Table 5, WhatsApp, Twitter"
value={form.tableName} value={form.tableName}
onChange={(e) => setForm((f) => ({ ...f, tableName: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, tableName: e.target.value }))}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Volunteer Name</Label> <Label>Person <span className="text-muted-foreground font-normal">(optional)</span></Label>
<Input <Input
placeholder="e.g. Ahmed" placeholder="e.g. Ahmed, Sarah"
value={form.volunteerName} value={form.volunteerName}
onChange={(e) => setForm((f) => ({ ...f, volunteerName: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, volunteerName: e.target.value }))}
/> />
</div> </div>
</div> </div>
<div className="space-y-2">
<Label>Label *</Label>
<Input
placeholder="e.g. Table 5 - Ahmed"
value={form.label}
onChange={(e) => setForm((f) => ({ ...f, label: e.target.value }))}
/>
<p className="text-xs text-muted-foreground">Auto-generated from table + volunteer, or enter custom</p>
</div>
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1"> <Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">Cancel</Button>
Cancel
</Button>
<Button onClick={handleCreate} disabled={!form.label || creating} className="flex-1"> <Button onClick={handleCreate} disabled={!form.label || creating} className="flex-1">
{creating ? "Creating..." : "Create QR Code"} {creating ? "Creating..." : "Create Link"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -53,7 +53,7 @@ export default function EventsPage() {
}, []) }, [])
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [orgType, setOrgType] = useState<string | null>(null) const [orgType, setOrgType] = useState<string | null>(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 // Fetch org type to customize the form
useEffect(() => { useEffect(() => {
@@ -77,15 +77,16 @@ export default function EventsPage() {
goalAmount: form.goalAmount ? Math.round(parseFloat(form.goalAmount) * 100) : undefined, goalAmount: form.goalAmount ? Math.round(parseFloat(form.goalAmount) * 100) : undefined,
eventDate: form.eventDate ? new Date(form.eventDate).toISOString() : undefined, eventDate: form.eventDate ? new Date(form.eventDate).toISOString() : undefined,
paymentMode: form.paymentMode, paymentMode: form.paymentMode,
externalUrl: form.paymentMode === "external" ? form.externalUrl : undefined, externalUrl: form.externalUrl || undefined,
externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined, externalPlatform: form.paymentMode === "external" ? (form.externalPlatform || "other") : undefined,
fundAllocation: form.fundAllocation || undefined,
}), }),
}) })
if (res.ok) { if (res.ok) {
const event = await res.json() const event = await res.json()
setEvents((prev) => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev]) setEvents((prev) => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev])
setShowCreate(false) 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 { } catch {
// handle error // handle error
@@ -97,11 +98,11 @@ export default function EventsPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-extrabold text-gray-900">Events</h1> <h1 className="text-3xl font-extrabold text-gray-900">Campaigns</h1>
<p className="text-muted-foreground mt-1">Manage your fundraising events and QR codes</p> <p className="text-muted-foreground mt-1">Create campaigns, share pledge links, and track donations</p>
</div> </div>
<Button onClick={() => setShowCreate(true)}> <Button onClick={() => setShowCreate(true)}>
<Plus className="h-4 w-4 mr-2" /> New Event <Plus className="h-4 w-4 mr-2" /> New Campaign
</Button> </Button>
</div> </div>
@@ -177,7 +178,7 @@ export default function EventsPage() {
<div className="flex gap-2"> <div className="flex gap-2">
<Link href={`/dashboard/events/${event.id}`} className="flex-1"> <Link href={`/dashboard/events/${event.id}`} className="flex-1">
<Button variant="outline" size="sm" className="w-full"> <Button variant="outline" size="sm" className="w-full">
<QrCode className="h-4 w-4 mr-1" /> QR Codes ({event.qrSourceCount}) <QrCode className="h-4 w-4 mr-1" /> Pledge Links ({event.qrSourceCount})
</Button> </Button>
</Link> </Link>
<Link href={`/dashboard/pledges?event=${event.id}`} className="flex-1"> <Link href={`/dashboard/pledges?event=${event.id}`} className="flex-1">
@@ -195,13 +196,13 @@ export default function EventsPage() {
{/* Create dialog */} {/* Create dialog */}
<Dialog open={showCreate} onOpenChange={setShowCreate}> <Dialog open={showCreate} onOpenChange={setShowCreate}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Event</DialogTitle> <DialogTitle>New Campaign</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Event Name *</Label> <Label>Campaign Name *</Label>
<Input <Input
placeholder="e.g. Ramadan Gala 2025" placeholder="e.g. Ramadan Appeal 2026, Mosque Building Fund"
value={form.name} value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
/> />
@@ -297,12 +298,38 @@ export default function EventsPage() {
</div> </div>
)} )}
{/* Fund allocation — for charities tracking which fund this goes to */}
{form.paymentMode === "self" && (
<div className="space-y-2">
<Label>Fund allocation <span className="text-muted-foreground font-normal">(optional)</span></Label>
<Input
placeholder="e.g. Mosque Building Fund, Orphan Sponsorship"
value={form.fundAllocation}
onChange={(e) => setForm(f => ({ ...f, fundAllocation: e.target.value }))}
/>
<p className="text-[10px] text-muted-foreground">Track which fund this event raises for. Shows on pledge receipts & reports.</p>
<div className="space-y-2">
<Label>External fundraising page <span className="text-muted-foreground font-normal">(optional)</span></Label>
<div className="relative">
<ExternalLink className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="https://launchgood.com/mosque-building"
value={form.externalUrl}
onChange={(e) => setForm(f => ({ ...f, externalUrl: e.target.value }))}
className="pl-9"
/>
</div>
<p className="text-[10px] text-muted-foreground">Link an external campaign page for reference & allocation tracking.</p>
</div>
</div>
)}
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1"> <Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">
Cancel Cancel
</Button> </Button>
<Button onClick={handleCreate} disabled={!form.name || creating || (form.paymentMode === "external" && !form.externalUrl)} className="flex-1"> <Button onClick={handleCreate} disabled={!form.name || creating || (form.paymentMode === "external" && !form.externalUrl)} className="flex-1">
{creating ? "Creating..." : "Create Event"} {creating ? "Creating..." : "Create Campaign"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -3,12 +3,12 @@
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { useSession, signOut } from "next-auth/react" 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" import { cn } from "@/lib/utils"
const navItems = [ const navItems = [
{ href: "/dashboard", label: "Overview", icon: LayoutDashboard }, { 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/pledges", label: "Pledges", icon: FileBarChart },
{ href: "/dashboard/reconcile", label: "Reconcile", icon: Upload }, { href: "/dashboard/reconcile", label: "Reconcile", icon: Upload },
{ href: "/dashboard/exports", label: "Exports", icon: Download }, { href: "/dashboard/exports", label: "Exports", icon: Download },
@@ -39,7 +39,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<div className="flex-1" /> <div className="flex-1" />
<Link href="/dashboard/events" className="hidden md:block"> <Link href="/dashboard/events" className="hidden md:block">
<button className="inline-flex items-center gap-1.5 rounded-lg bg-trust-blue px-3 py-1.5 text-xs font-semibold text-white hover:bg-trust-blue/90 transition-colors"> <button className="inline-flex items-center gap-1.5 rounded-lg bg-trust-blue px-3 py-1.5 text-xs font-semibold text-white hover:bg-trust-blue/90 transition-colors">
<Plus className="h-3 w-3" /> New Event <Plus className="h-3 w-3" /> New Campaign
</button> </button>
</Link> </Link>
<Link href="/" className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"> <Link href="/" className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1">

View File

@@ -302,7 +302,7 @@ export default function DashboardPage() {
</p> </p>
<div className="flex gap-2 justify-center pt-2"> <div className="flex gap-2 justify-center pt-2">
<Link href="/dashboard/events"> <Link href="/dashboard/events">
<Button size="sm">Create a Fundraiser </Button> <Button size="sm">Create a Campaign </Button>
</Link> </Link>
</div> </div>
</CardContent> </CardContent>

View File

@@ -21,6 +21,8 @@ interface OrgSettings {
primaryColor: string primaryColor: string
gcAccessToken: string gcAccessToken: string
gcEnvironment: string gcEnvironment: string
orgType: string
zakatEnabled: boolean
} }
export default function SettingsPage() { export default function SettingsPage() {
@@ -116,6 +118,42 @@ export default function SettingsPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Zakat & Fund Types */}
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2"> Zakat & Fund Types</CardTitle>
<CardDescription className="text-xs">Let donors specify their donation type (Zakat, Sadaqah, Lillah, Fitrana)</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<button
onClick={() => {
setSettings(s => s ? { ...s, zakatEnabled: !s.zakatEnabled } : s)
fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ zakatEnabled: !settings.zakatEnabled }) }).then(() => { setSaved("zakat"); setTimeout(() => setSaved(null), 2000) }).catch(() => {})
}}
className={`w-full flex items-center justify-between rounded-xl border-2 p-4 transition-all ${
settings.zakatEnabled ? "border-trust-blue bg-trust-blue/5" : "border-gray-200"
}`}
>
<div className="text-left">
<p className="text-sm font-bold">{settings.zakatEnabled ? "Fund types enabled" : "Enable fund types"}</p>
<p className="text-xs text-muted-foreground mt-0.5">Donors can choose: Zakat · Sadaqah · Lillah · Fitrana · General</p>
</div>
<div className={`w-11 h-6 rounded-full transition-colors ${settings.zakatEnabled ? "bg-trust-blue" : "bg-gray-200"}`}>
<div className={`w-5 h-5 bg-white rounded-full shadow-sm mt-0.5 transition-transform ${settings.zakatEnabled ? "translate-x-5.5 ml-[22px]" : "translate-x-0.5 ml-[2px]"}`} />
</div>
</button>
{settings.zakatEnabled && (
<div className="rounded-xl bg-trust-blue/5 border border-trust-blue/10 p-3 space-y-1.5 text-xs text-muted-foreground animate-fade-in">
<p>🌙 <strong>Zakat</strong> Obligatory 2.5% annual charity</p>
<p>🤲 <strong>Sadaqah / General</strong> Voluntary donations</p>
<p>🌱 <strong>Sadaqah Jariyah</strong> Ongoing charity (buildings, wells)</p>
<p>🕌 <strong>Lillah</strong> For the mosque / institution</p>
<p>🍽 <strong>Fitrana</strong> Zakat al-Fitr (before Eid)</p>
</div>
)}
</CardContent>
</Card>
{/* Branding */} {/* Branding */}
<Card> <Card>
<CardHeader> <CardHeader>

View File

@@ -21,6 +21,7 @@ export interface PledgeData {
donorEmail: string donorEmail: string
donorPhone: string donorPhone: string
giftAid: boolean giftAid: boolean
fundType?: string
// Scheduling // Scheduling
scheduleMode: "now" | "date" | "installments" scheduleMode: "now" | "date" | "installments"
dueDate?: string dueDate?: string
@@ -37,6 +38,8 @@ interface EventInfo {
paymentMode: "self" | "external" paymentMode: "self" | "external"
externalUrl: string | null externalUrl: string | null
externalPlatform: 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) // 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 } const finalData = { ...pledgeData, ...identity }
setPledgeData(finalData) setPledgeData(finalData)
@@ -146,6 +149,7 @@ export default function PledgePage() {
...finalData, ...finalData,
eventId: eventInfo?.id, eventId: eventInfo?.id,
qrSourceId: eventInfo?.qrSourceId, qrSourceId: eventInfo?.qrSourceId,
fundType: finalData.fundType || undefined,
}), }),
}) })
const result = await res.json() const result = await res.json()
@@ -205,7 +209,7 @@ export default function PledgePage() {
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />, 0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />, 1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />, 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} />, 4: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} donorPhone={pledgeData.donorPhone} />,
5: pledgeResult && ( 5: pledgeResult && (
<ConfirmationStep <ConfirmationStep

View File

@@ -4,21 +4,33 @@ import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Gift, Shield, Sparkles, Phone, Mail } from "lucide-react" 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 { interface Props {
onSubmit: (data: { onSubmit: (data: {
donorName: string donorName: string
donorEmail: string donorEmail: string
donorPhone: string donorPhone: string
giftAid: boolean giftAid: boolean
fundType?: string
}) => void }) => void
amount: number 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 [name, setName] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [phone, setPhone] = useState("") const [phone, setPhone] = useState("")
const [giftAid, setGiftAid] = useState(false) const [giftAid, setGiftAid] = useState(false)
const [fundType, setFundType] = useState<string>(fundAllocation ? "general" : "general")
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [contactMode, setContactMode] = useState<"email" | "phone">("email") const [contactMode, setContactMode] = useState<"email" | "phone">("email")
const nameRef = useRef<HTMLInputElement>(null) const nameRef = useRef<HTMLInputElement>(null)
@@ -34,7 +46,7 @@ export function IdentityStep({ onSubmit, amount }: Props) {
if (!isValid) return if (!isValid) return
setSubmitting(true) setSubmitting(true)
try { try {
await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid }) await onSubmit({ donorName: name, donorEmail: email, donorPhone: phone, giftAid, fundType: zakatEnabled ? fundType : undefined })
} catch { } catch {
setSubmitting(false) setSubmitting(false)
} }
@@ -122,6 +134,37 @@ export function IdentityStep({ onSubmit, amount }: Props) {
)} )}
</div> </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 */} {/* Gift Aid — the hero */}
<button <button
onClick={() => setGiftAid(!giftAid)} onClick={() => setGiftAid(!giftAid)}

View File

@@ -13,12 +13,8 @@ export default function HomePage() {
<span className="font-black text-sm">Pledge Now, Pay Later</span> <span className="font-black text-sm">Pledge Now, Pay Later</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/login" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"> <Link href="/login" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">Sign In</Link>
Sign In <Link href="/signup" className="rounded-lg bg-trust-blue px-4 py-2 text-sm font-semibold text-white hover:bg-trust-blue/90 transition-colors">Get Started Free</Link>
</Link>
<Link href="/signup" className="rounded-lg bg-trust-blue px-4 py-2 text-sm font-semibold text-white hover:bg-trust-blue/90 transition-colors">
Get Started Free
</Link>
</div> </div>
</div> </div>
</header> </header>
@@ -27,15 +23,14 @@ export default function HomePage() {
<section className="py-16 md:py-24 px-4"> <section className="py-16 md:py-24 px-4">
<div className="max-w-3xl mx-auto text-center space-y-6"> <div className="max-w-3xl mx-auto text-center space-y-6">
<div className="inline-flex items-center gap-2 bg-trust-blue/5 border border-trust-blue/20 rounded-full px-4 py-1.5 text-xs font-medium text-trust-blue"> <div className="inline-flex items-center gap-2 bg-trust-blue/5 border border-trust-blue/20 rounded-full px-4 py-1.5 text-xs font-medium text-trust-blue">
🇬🇧 Built for UK charities · Free to start 🇬🇧 Built for UK charities &amp; fundraisers · Free forever
</div> </div>
<h1 className="text-4xl md:text-5xl font-black text-gray-900 leading-tight"> <h1 className="text-4xl md:text-6xl font-black text-gray-900 leading-[1.1]">
Turn promises into Collect pledges.<br />
<span className="text-trust-blue"> payments</span> <span className="text-trust-blue">Convert them into donations.</span>
</h1> </h1>
<p className="text-lg text-muted-foreground max-w-xl mx-auto"> <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
At your next event, donors pledge what they want to give then pay on their own terms. Someone says &quot;I&apos;ll donate £5,000&quot; 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.
You get QR codes, WhatsApp reminders, and a dashboard to track every penny.
</p> </p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link href="/signup" className="rounded-xl bg-trust-blue px-6 py-3.5 text-base font-semibold text-white hover:bg-trust-blue/90 transition-all shadow-lg shadow-trust-blue/20"> <Link href="/signup" className="rounded-xl bg-trust-blue px-6 py-3.5 text-base font-semibold text-white hover:bg-trust-blue/90 transition-all shadow-lg shadow-trust-blue/20">
@@ -49,50 +44,130 @@ export default function HomePage() {
</div> </div>
</section> </section>
{/* Problem */} {/* Use Cases — the 4 audiences */}
<section className="py-12 bg-gray-50 px-4"> <section className="py-14 bg-gray-50 px-4">
<div className="max-w-4xl mx-auto"> <div className="max-w-5xl mx-auto space-y-8">
<div className="text-center">
<h2 className="text-3xl font-black text-gray-900">Who uses PNPL?</h2>
<p className="text-muted-foreground mt-2">Anyone who collects promises of money and needs them fulfilled.</p>
</div>
<div className="grid md:grid-cols-2 gap-5">
{/* Charity at events */}
<div className="bg-white rounded-2xl p-6 border space-y-3">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-trust-blue/10 flex items-center justify-center text-xl">🕌</div>
<div>
<h3 className="font-bold">Charities &amp; Mosques at Events</h3>
<p className="text-xs text-muted-foreground">Gala dinners, Ramadan nights, Jumuah appeals</p>
</div>
</div>
<p className="text-sm text-muted-foreground">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.</p>
<div className="flex flex-wrap gap-1.5">
<span className="text-[10px] font-medium bg-trust-blue/5 text-trust-blue px-2 py-0.5 rounded-full">QR per table</span>
<span className="text-[10px] font-medium bg-trust-blue/5 text-trust-blue px-2 py-0.5 rounded-full">Bank transfer</span>
<span className="text-[10px] font-medium bg-trust-blue/5 text-trust-blue px-2 py-0.5 rounded-full">Gift Aid</span>
<span className="text-[10px] font-medium bg-trust-blue/5 text-trust-blue px-2 py-0.5 rounded-full">Zakat tracking</span>
</div>
</div>
{/* HNW donors */}
<div className="bg-white rounded-2xl p-6 border space-y-3">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-warm-amber/10 flex items-center justify-center text-xl">💎</div>
<div>
<h3 className="font-bold">High-Net-Worth Donor Outreach</h3>
<p className="text-xs text-muted-foreground">Major gifts, personal pledges, board commitments</p>
</div>
</div>
<p className="text-sm text-muted-foreground">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.</p>
<div className="flex flex-wrap gap-1.5">
<span className="text-[10px] font-medium bg-warm-amber/5 text-warm-amber px-2 py-0.5 rounded-full">Personal links</span>
<span className="text-[10px] font-medium bg-warm-amber/5 text-warm-amber px-2 py-0.5 rounded-full">Instalments</span>
<span className="text-[10px] font-medium bg-warm-amber/5 text-warm-amber px-2 py-0.5 rounded-full">WhatsApp follow-up</span>
</div>
</div>
{/* Org-to-org */}
<div className="bg-white rounded-2xl p-6 border space-y-3">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-success-green/10 flex items-center justify-center text-xl">🏗</div>
<div>
<h3 className="font-bold">Org-to-Org Pledges</h3>
<p className="text-xs text-muted-foreground">Multi-charity projects, umbrella fundraising</p>
</div>
</div>
<p className="text-sm text-muted-foreground">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&apos;s delivered.</p>
<div className="flex flex-wrap gap-1.5">
<span className="text-[10px] font-medium bg-success-green/5 text-success-green px-2 py-0.5 rounded-full">Fund allocation</span>
<span className="text-[10px] font-medium bg-success-green/5 text-success-green px-2 py-0.5 rounded-full">Multi-party</span>
<span className="text-[10px] font-medium bg-success-green/5 text-success-green px-2 py-0.5 rounded-full">Project tracking</span>
</div>
</div>
{/* Personal fundraiser */}
<div className="bg-white rounded-2xl p-6 border space-y-3">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-purple-100 flex items-center justify-center text-xl"></div>
<div>
<h3 className="font-bold">Personal Fundraisers</h3>
<p className="text-xs text-muted-foreground">LaunchGood, Enthuse, JustGiving, GoFundMe</p>
</div>
</div>
<p className="text-sm text-muted-foreground">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.</p>
<div className="flex flex-wrap gap-1.5">
<span className="text-[10px] font-medium bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full">External redirect</span>
<span className="text-[10px] font-medium bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full">Social sharing</span>
<span className="text-[10px] font-medium bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full">Conversion tracking</span>
</div>
</div>
</div>
</div>
</section>
{/* The Problem */}
<section className="py-14 px-4">
<div className="max-w-4xl mx-auto space-y-8">
<div className="text-center">
<p className="text-sm font-bold text-danger-red uppercase tracking-wider">The Problem</p>
<h2 className="text-3xl font-black text-gray-900 mt-2">30-50% of pledges never convert</h2>
</div>
<div className="grid md:grid-cols-3 gap-6"> <div className="grid md:grid-cols-3 gap-6">
<div className="bg-white rounded-2xl p-6 border"> <div className="bg-white rounded-2xl p-6 border">
<div className="text-3xl mb-3">😤</div> <div className="text-3xl mb-3">😤</div>
<h3 className="font-bold mb-1">Pledges go cold</h3> <h3 className="font-bold mb-1">No follow-up system</h3>
<p className="text-sm text-muted-foreground">Donors say &quot;I&apos;ll pay £500&quot; at the gala, then forget. You have no way to follow up.</p> <p className="text-sm text-muted-foreground">Someone pledges £5,000 at your dinner. You write it on a napkin. A week later who pledged what? No idea.</p>
</div>
<div className="bg-white rounded-2xl p-6 border">
<div className="text-3xl mb-3">📝</div>
<h3 className="font-bold mb-1">Paper tracking</h3>
<p className="text-sm text-muted-foreground">Spreadsheets, napkin notes, WhatsApp groups. No system, no references, no proof.</p>
</div> </div>
<div className="bg-white rounded-2xl p-6 border"> <div className="bg-white rounded-2xl p-6 border">
<div className="text-3xl mb-3">💸</div> <div className="text-3xl mb-3">💸</div>
<h3 className="font-bold mb-1">Money left on the table</h3> <h3 className="font-bold mb-1">Money left on the table</h3>
<p className="text-sm text-muted-foreground">UK charities lose 30-50% of pledged amounts because there&apos;s no follow-up system.</p> <p className="text-sm text-muted-foreground">Donors meant it when they said it. But without a reminder and easy payment path, life gets in the way.</p>
</div>
<div className="bg-white rounded-2xl p-6 border">
<div className="text-3xl mb-3">🕌</div>
<h3 className="font-bold mb-1">Funds mixed up</h3>
<p className="text-sm text-muted-foreground">Zakat mixed with Sadaqah. Building fund mixed with general. No audit trail for fund allocation.</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{/* How it works */} {/* How it works */}
<section id="how" className="py-16 px-4"> <section className="py-14 bg-gray-50 px-4">
<div className="max-w-4xl mx-auto space-y-10"> <div className="max-w-4xl mx-auto space-y-10">
<div className="text-center"> <div className="text-center">
<h2 className="text-3xl font-black text-gray-900">How it works</h2> <h2 className="text-3xl font-black text-gray-900">How it works</h2>
<p className="text-muted-foreground mt-2">From pledge to payment in 4 steps</p> <p className="text-muted-foreground mt-2">Whether it&apos;s a QR code at a gala or a link in a WhatsApp group</p>
</div> </div>
<div className="grid md:grid-cols-4 gap-6"> <div className="grid md:grid-cols-4 gap-6">
{[ {[
{ step: "1", icon: "📱", title: "Donor scans QR", desc: "At your event, each table/volunteer has a unique QR code." }, { 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: "Pledges amount", desc: "Pick an amount. Choose to pay now, on a date, or monthly instalments." }, { 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: "Gets reminders", desc: "WhatsApp messages with bank details before each due date. They reply PAID when done." }, { 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 reconcile", desc: "Dashboard shows who pledged, who paid, who needs a nudge. Upload bank statements to auto-match." }, { 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) => ( ].map((s) => (
<div key={s.step} className="text-center space-y-2"> <div key={s.step} className="text-center space-y-2">
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-trust-blue/5 text-2xl"> <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-trust-blue/5 text-2xl">{s.icon}</div>
{s.icon} <div className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-trust-blue text-white text-xs font-bold">{s.step}</div>
</div>
<div className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-trust-blue text-white text-xs font-bold">
{s.step}
</div>
<h3 className="font-bold text-sm">{s.title}</h3> <h3 className="font-bold text-sm">{s.title}</h3>
<p className="text-xs text-muted-foreground">{s.desc}</p> <p className="text-xs text-muted-foreground">{s.desc}</p>
</div> </div>
@@ -101,25 +176,101 @@ export default function HomePage() {
</div> </div>
</section> </section>
{/* Sharing channels */}
<section className="py-14 px-4">
<div className="max-w-3xl mx-auto text-center space-y-8">
<h2 className="text-3xl font-black text-gray-900">Share anywhere. Track everything.</h2>
<p className="text-muted-foreground">Every link is unique and trackable. See exactly where each pledge came from.</p>
<div className="grid grid-cols-3 md:grid-cols-6 gap-3">
{[
{ icon: "💬", label: "WhatsApp" },
{ icon: "📱", label: "QR Code" },
{ icon: "📧", label: "Email" },
{ icon: "📸", label: "Instagram" },
{ icon: "🐦", label: "Twitter/X" },
{ icon: "👤", label: "1-on-1" },
].map((c) => (
<div key={c.label} className="rounded-xl border bg-white p-3 text-center space-y-1">
<span className="text-xl">{c.icon}</span>
<p className="text-[11px] font-medium">{c.label}</p>
</div>
))}
</div>
</div>
</section>
{/* Platforms */}
<section className="py-14 bg-gray-50 px-4">
<div className="max-w-3xl mx-auto text-center space-y-8">
<h2 className="text-3xl font-black text-gray-900">Works with your payment platform</h2>
<p className="text-muted-foreground">Process donations directly, or redirect donors to your existing page.</p>
<div className="flex flex-wrap justify-center gap-3">
{[
{ 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) => (
<div key={p.name} className="flex items-center gap-2 rounded-xl border px-4 py-3 bg-white" style={{ borderColor: p.color + "30" }}>
<span className="text-lg">{p.icon}</span>
<span className="text-sm font-medium">{p.name}</span>
</div>
))}
</div>
</div>
</section>
{/* Fund Types */}
<section className="py-14 px-4">
<div className="max-w-4xl mx-auto space-y-8">
<div className="text-center">
<h2 className="text-3xl font-black text-gray-900">Islamic fund types built-in</h2>
<p className="text-muted-foreground mt-2">Donors choose their fund type. You get clean reporting. Zakat never mixes with Sadaqah.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{[
{ 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) => (
<div key={f.name} className="rounded-2xl border bg-white p-4 text-center space-y-1">
<span className="text-2xl">{f.icon}</span>
<p className="text-sm font-bold">{f.name}</p>
<p className="text-[11px] text-muted-foreground">{f.desc}</p>
</div>
))}
</div>
<p className="text-center text-xs text-muted-foreground">
Enable in Settings Fund Types. Donors see the picker during pledge. Reports break down by type.
</p>
</div>
</section>
{/* Features */} {/* Features */}
<section className="py-16 bg-gray-50 px-4"> <section className="py-14 bg-gray-50 px-4">
<div className="max-w-4xl mx-auto space-y-10"> <div className="max-w-4xl mx-auto space-y-10">
<div className="text-center"> <div className="text-center">
<h2 className="text-3xl font-black text-gray-900">Everything you need</h2> <h2 className="text-3xl font-black text-gray-900">Everything you need</h2>
</div> </div>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
{[ {[
{ icon: "📱", title: "QR Code Generator", desc: "Unique codes per volunteer/table. Track who brings in the most." }, { 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." }, { 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: "Auto-send bank details and reminders. Donors reply PAID, HELP, or CANCEL." }, { 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", desc: "Collect declarations inline. Export HMRC-ready CSV with one click." }, { icon: "🎁", title: "Gift Aid + HMRC Export", desc: "Collect declarations inline with live math. One-click HMRC-ready CSV export." },
{ icon: "🏦", title: "UK Bank Transfers", desc: "Unique reference per pledge for easy reconciliation. Tap-to-copy details." }, { 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: "See pledges come in real-time. Pipeline view: pending → initiated → paid." }, { icon: "📊", title: "Live Dashboard", desc: "Real-time pipeline: new → initiated → paid → overdue. Needs-attention alerts. Auto-refreshes." },
{ icon: "🏆", title: "Volunteer Leaderboard", desc: "Real-time scoreboard. Motivate your team with friendly competition." }, { icon: "🏦", title: "Fund Allocation", desc: "Link campaigns to specific funds. Charities can also link external fundraising pages for allocation tracking." },
{ icon: "📤", title: "CRM Export", desc: "Download all pledge data as CSV. Gift Aid pack for HMRC." }, { 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) => ( ].map((f) => (
<div key={f.title} className="bg-white rounded-xl p-4 border flex gap-3 items-start"> <div key={f.title} className="bg-white rounded-xl p-4 border flex gap-3 items-start hover:shadow-sm transition-shadow">
<span className="text-xl">{f.icon}</span> <span className="text-xl flex-shrink-0">{f.icon}</span>
<div> <div>
<h3 className="font-bold text-sm">{f.title}</h3> <h3 className="font-bold text-sm">{f.title}</h3>
<p className="text-xs text-muted-foreground mt-0.5">{f.desc}</p> <p className="text-xs text-muted-foreground mt-0.5">{f.desc}</p>
@@ -131,24 +282,24 @@ export default function HomePage() {
</section> </section>
{/* Donor schedule */} {/* Donor schedule */}
<section className="py-16 px-4"> <section className="py-14 px-4">
<div className="max-w-3xl mx-auto text-center space-y-8"> <div className="max-w-3xl mx-auto text-center space-y-8">
<h2 className="text-3xl font-black text-gray-900">Donors choose when to pay</h2> <h2 className="text-3xl font-black text-gray-900">Donors choose when to pay</h2>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="rounded-2xl border-2 border-trust-blue/20 p-5 space-y-2"> <div className="rounded-2xl border-2 border-trust-blue/20 p-5 space-y-2 bg-white">
<div className="text-3xl"></div> <div className="text-3xl"></div>
<h3 className="font-bold">Pay Now</h3> <h3 className="font-bold">Pay Now</h3>
<p className="text-xs text-muted-foreground">Card, bank transfer, or Direct Debit right away</p> <p className="text-xs text-muted-foreground">Bank transfer or redirect to your fundraising page</p>
</div> </div>
<div className="rounded-2xl border-2 border-warm-amber/20 p-5 space-y-2"> <div className="rounded-2xl border-2 border-warm-amber/20 p-5 space-y-2 bg-white">
<div className="text-3xl">📅</div> <div className="text-3xl">📅</div>
<h3 className="font-bold">Pick a Date</h3> <h3 className="font-bold">Pick a Date</h3>
<p className="text-xs text-muted-foreground">&quot;I&apos;ll pay on payday&quot; reminders sent automatically</p> <p className="text-xs text-muted-foreground">&quot;I&apos;ll pay on payday&quot; WhatsApp reminders sent automatically</p>
</div> </div>
<div className="rounded-2xl border-2 border-success-green/20 p-5 space-y-2"> <div className="rounded-2xl border-2 border-success-green/20 p-5 space-y-2 bg-white">
<div className="text-3xl">📆</div> <div className="text-3xl">📆</div>
<h3 className="font-bold">Monthly</h3> <h3 className="font-bold">Monthly</h3>
<p className="text-xs text-muted-foreground">Split into 2-12 instalments. Each one tracked separately</p> <p className="text-xs text-muted-foreground">Split into 2-12 instalments. Each one tracked &amp; reminded</p>
</div> </div>
</div> </div>
</div> </div>
@@ -157,14 +308,18 @@ export default function HomePage() {
{/* CTA */} {/* CTA */}
<section className="py-16 bg-gradient-to-br from-trust-blue to-blue-600 px-4"> <section className="py-16 bg-gradient-to-br from-trust-blue to-blue-600 px-4">
<div className="max-w-2xl mx-auto text-center space-y-6"> <div className="max-w-2xl mx-auto text-center space-y-6">
<h2 className="text-3xl font-black text-white">Start collecting pledges today</h2> <h2 className="text-3xl font-black text-white">Stop losing pledges.</h2>
<p className="text-blue-100"> <p className="text-blue-100 max-w-lg mx-auto">
Free to use. Set up in 2 minutes. No technical knowledge needed. Free to use. Set up in 2 minutes. Whether you&apos;re collecting pledges at a gala dinner, from your WhatsApp contacts, or from other organisations for a joint project.
</p> </p>
<Link href="/signup" className="inline-block rounded-xl bg-white px-8 py-4 text-base font-bold text-trust-blue hover:bg-blue-50 transition-all shadow-xl"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
Create Your Free Account <Link href="/signup" className="inline-block rounded-xl bg-white px-8 py-4 text-base font-bold text-trust-blue hover:bg-blue-50 transition-all shadow-xl">
</Link> Create Your Free Account
<p className="text-xs text-blue-200">Used by mosques, churches, schools and charities across the UK</p> </Link>
<Link href="/login?demo=1" className="inline-block rounded-xl border-2 border-white/30 px-8 py-4 text-base font-bold text-white hover:bg-white/10 transition-all">
🎮 Try the Demo
</Link>
</div>
</div> </div>
</section> </section>
@@ -172,14 +327,13 @@ export default function HomePage() {
<footer className="py-8 px-4 border-t"> <footer className="py-8 px-4 border-t">
<div className="max-w-4xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4 text-xs text-muted-foreground"> <div className="max-w-4xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-lg bg-trust-blue flex items-center justify-center"> <div className="h-6 w-6 rounded-lg bg-trust-blue flex items-center justify-center"><span className="text-white text-[10px]">🤲</span></div>
<span className="text-white text-[10px]">🤲</span>
</div>
<span>Pledge Now, Pay Later</span> <span>Pledge Now, Pay Later</span>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<Link href="/login" className="hover:text-foreground">Sign In</Link> <Link href="/login" className="hover:text-foreground">Sign In</Link>
<Link href="/signup" className="hover:text-foreground">Get Started</Link> <Link href="/signup" className="hover:text-foreground">Get Started</Link>
<Link href="/login?demo=1" className="hover:text-foreground">Demo</Link>
</div> </div>
<span>© {new Date().getFullYear()} QuikCue Ltd</span> <span>© {new Date().getFullYear()} QuikCue Ltd</span>
</div> </div>

View File

@@ -10,6 +10,7 @@ export const createEventSchema = z.object({
paymentMode: z.enum(['self', 'external']).default('self'), paymentMode: z.enum(['self', 'external']).default('self'),
externalUrl: z.string().url().max(1000).optional(), externalUrl: z.string().url().max(1000).optional(),
externalPlatform: z.enum(['launchgood', 'enthuse', 'justgiving', 'gofundme', 'other']).optional(), externalPlatform: z.enum(['launchgood', 'enthuse', 'justgiving', 'gofundme', 'other']).optional(),
fundAllocation: z.string().max(200).optional(),
}) })
export const createQrSourceSchema = z.object({ export const createQrSourceSchema = z.object({
@@ -25,6 +26,7 @@ export const createPledgeSchema = z.object({
donorEmail: z.string().max(200).optional().default(''), donorEmail: z.string().max(200).optional().default(''),
donorPhone: z.string().max(20).optional().default(''), donorPhone: z.string().max(20).optional().default(''),
giftAid: z.boolean().default(false), giftAid: z.boolean().default(false),
fundType: z.enum(['general', 'zakat', 'sadaqah', 'lillah', 'fitrana']).optional(),
eventId: z.string(), eventId: z.string(),
qrSourceId: z.string().nullable().optional(), qrSourceId: z.string().nullable().optional(),
// Payment scheduling // Payment scheduling