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:
@@ -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<QrSourceInfo[]>([])
|
||||
const [sources, setSources] = useState<SourceInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [copiedCode, setCopiedCode] = useState<string | null>(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 (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-8 w-8 text-trust-blue animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (loading) 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 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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Link
|
||||
href="/dashboard/events"
|
||||
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 href="/dashboard/events" 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 Campaigns
|
||||
</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">
|
||||
{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>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/dashboard/events/${eventId}/leaderboard`}>
|
||||
<Button variant="outline">
|
||||
<Trophy className="h-4 w-4 mr-2" /> Leaderboard
|
||||
</Button>
|
||||
<Button variant="outline"><Trophy className="h-4 w-4 mr-2" /> Leaderboard</Button>
|
||||
</Link>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QR Grid */}
|
||||
{qrSources.length === 0 ? (
|
||||
{sources.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center space-y-4">
|
||||
<QrCode 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>
|
||||
<Link2 className="h-12 w-12 text-muted-foreground mx-auto" />
|
||||
<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)}>
|
||||
<Plus className="h-4 w-4 mr-2" /> Create First QR Code
|
||||
<Plus className="h-4 w-4 mr-2" /> Create Pledge Link
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{qrSources.map((qr) => (
|
||||
<Card key={qr.id} className="hover:shadow-md transition-shadow">
|
||||
{sources.map((src) => (
|
||||
<Card key={src.id} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
{/* QR Code */}
|
||||
<div className="max-w-[180px] mx-auto bg-white rounded-2xl flex items-center justify-center p-2">
|
||||
<QRCodeCanvas url={`${baseUrl}/p/${qr.code}`} size={164} />
|
||||
{/* QR Code — compact */}
|
||||
<div className="max-w-[140px] mx-auto bg-white rounded-xl flex items-center justify-center p-1.5">
|
||||
<QRCodeCanvas url={`${baseUrl}/p/${src.code}`} size={128} />
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="font-bold">{qr.label}</h3>
|
||||
{qr.volunteerName && (
|
||||
<p className="text-xs text-muted-foreground">Volunteer: {qr.volunteerName}</p>
|
||||
)}
|
||||
<h3 className="font-bold">{src.label}</h3>
|
||||
{src.volunteerName && <p className="text-xs text-muted-foreground">By: {src.volunteerName}</p>}
|
||||
<p className="text-[10px] text-muted-foreground font-mono mt-1">{baseUrl}/p/{src.code}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div className="rounded-lg bg-gray-50 p-2">
|
||||
<p className="font-bold text-sm">{qr.scanCount}</p>
|
||||
<p className="text-muted-foreground">Scans</p>
|
||||
<p className="font-bold text-sm">{src.scanCount}</p>
|
||||
<p className="text-muted-foreground">Clicks</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 p-2">
|
||||
<p className="font-bold text-sm">{formatPence(qr.totalPledged)}</p>
|
||||
<p className="text-muted-foreground">Total</p>
|
||||
<p className="font-bold text-sm">{formatPence(src.totalPledged)}</p>
|
||||
<p className="text-muted-foreground">Raised</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conversion rate */}
|
||||
{qr.scanCount > 0 && (
|
||||
{src.scanCount > 0 && (
|
||||
<div className="text-center">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => copyLink(qr.code)}
|
||||
>
|
||||
{copiedCode === qr.code ? (
|
||||
<><Check className="h-3 w-3 mr-1" /> Copied</>
|
||||
) : (
|
||||
<><Copy className="h-3 w-3 mr-1" /> Link</>
|
||||
)}
|
||||
{/* Share options — the main CTA, not an afterthought */}
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
<Button variant="outline" size="sm" onClick={() => copyLink(src.code)} className="text-[10px] px-1">
|
||||
{copiedCode === src.code ? <Check className="h-3.5 w-3.5" /> : <><Copy className="h-3 w-3" /> Copy</>}
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => shareWhatsApp(src.code, src.label)} className="text-[10px] px-1 bg-[#25D366] hover:bg-[#20BD5A] text-white">
|
||||
<MessageCircle className="h-3 w-3" /> WA
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => shareEmail(src.code, src.label)} className="text-[10px] px-1">
|
||||
<Mail className="h-3 w-3" /> Email
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => shareLink(src.code, src.label)} className="text-[10px] px-1">
|
||||
<Share2 className="h-3 w-3" /> More
|
||||
</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>
|
||||
{/* Volunteer & share links */}
|
||||
|
||||
{/* Secondary actions */}
|
||||
<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">
|
||||
<Users className="h-3 w-3 mr-1" /> Volunteer View
|
||||
<Download className="h-3 w-3 mr-1" /> QR Image
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs bg-[#25D366]/5 border-[#25D366]/30 text-[#25D366] hover:bg-[#25D366]/10"
|
||||
onClick={() => {
|
||||
const url = `${baseUrl}/p/${qr.code}`
|
||||
const text = `Hi! Scan this to pledge: ${url}`
|
||||
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank")
|
||||
}}
|
||||
>
|
||||
<MessageCircle className="h-3 w-3 mr-1" /> Share
|
||||
</Button>
|
||||
<a href={`/v/${src.code}`} target="_blank" className="flex-1">
|
||||
<Button variant="outline" size="sm" className="w-full text-xs">
|
||||
<Users className="h-3 w-3 mr-1" /> Volunteer
|
||||
</Button>
|
||||
</a>
|
||||
<a href={`/p/${src.code}`} target="_blank">
|
||||
<Button variant="outline" size="sm"><ExternalLink className="h-3 w-3" /></Button>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -223,42 +216,42 @@ export default function EventQRPage() {
|
||||
{/* Create dialog */}
|
||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create QR Code</DialogTitle>
|
||||
<DialogTitle>Create Pledge Link</DialogTitle>
|
||||
</DialogHeader>
|
||||
<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="space-y-2">
|
||||
<Label>Table Name</Label>
|
||||
<Label>Source / Channel <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
||||
<Input
|
||||
placeholder="e.g. Table 5"
|
||||
placeholder="e.g. Table 5, WhatsApp, Twitter"
|
||||
value={form.tableName}
|
||||
onChange={(e) => setForm((f) => ({ ...f, tableName: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Volunteer Name</Label>
|
||||
<Label>Person <span className="text-muted-foreground font-normal">(optional)</span></Label>
|
||||
<Input
|
||||
placeholder="e.g. Ahmed"
|
||||
placeholder="e.g. Ahmed, Sarah"
|
||||
value={form.volunteerName}
|
||||
onChange={(e) => setForm((f) => ({ ...f, volunteerName: e.target.value }))}
|
||||
/>
|
||||
</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">
|
||||
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">Cancel</Button>
|
||||
<Button onClick={handleCreate} disabled={!form.label || creating} className="flex-1">
|
||||
{creating ? "Creating..." : "Create QR Code"}
|
||||
{creating ? "Creating..." : "Create Link"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function EventsPage() {
|
||||
}, [])
|
||||
const [creating, setCreating] = useState(false)
|
||||
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
|
||||
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() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold text-gray-900">Events</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage your fundraising events and QR codes</p>
|
||||
<h1 className="text-3xl font-extrabold text-gray-900">Campaigns</h1>
|
||||
<p className="text-muted-foreground mt-1">Create campaigns, share pledge links, and track donations</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -177,7 +178,7 @@ export default function EventsPage() {
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/dashboard/events/${event.id}`} className="flex-1">
|
||||
<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>
|
||||
</Link>
|
||||
<Link href={`/dashboard/pledges?event=${event.id}`} className="flex-1">
|
||||
@@ -195,13 +196,13 @@ export default function EventsPage() {
|
||||
{/* Create dialog */}
|
||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Event</DialogTitle>
|
||||
<DialogTitle>New Campaign</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Event Name *</Label>
|
||||
<Label>Campaign Name *</Label>
|
||||
<Input
|
||||
placeholder="e.g. Ramadan Gala 2025"
|
||||
placeholder="e.g. Ramadan Appeal 2026, Mosque Building Fund"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
/>
|
||||
@@ -297,12 +298,38 @@ export default function EventsPage() {
|
||||
</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">
|
||||
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!form.name || creating || (form.paymentMode === "external" && !form.externalUrl)} className="flex-1">
|
||||
{creating ? "Creating..." : "Create Event"}
|
||||
{creating ? "Creating..." : "Create Campaign"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
<div className="flex-1" />
|
||||
<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">
|
||||
<Plus className="h-3 w-3" /> New Event
|
||||
<Plus className="h-3 w-3" /> New Campaign
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/" className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1">
|
||||
|
||||
@@ -302,7 +302,7 @@ export default function DashboardPage() {
|
||||
</p>
|
||||
<div className="flex gap-2 justify-center pt-2">
|
||||
<Link href="/dashboard/events">
|
||||
<Button size="sm">Create a Fundraiser →</Button>
|
||||
<Button size="sm">Create a Campaign →</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -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() {
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user