feat: remove FPX, add UK charity persona features

- Remove FPX payment rail entirely (Malaysian, not UK)
- Add volunteer portal (/v/[code]) with live pledge tracking
- Add public event page (/e/[slug]) with progress bar + social proof
- Add fundraiser leaderboard (/dashboard/events/[id]/leaderboard)
- Add WhatsApp share buttons on confirmation, bank instructions, volunteer view
- Enhanced Gift Aid UX with +25% bonus display and HMRC declaration text
- Gift Aid report export (HMRC-ready CSV filter)
- Volunteer view link + WhatsApp share on QR code cards
- Updated home page: 4 personas, 3 UK payment rails, 8 features
- Public event API endpoint with privacy-safe donor name truncation
- Volunteer API with stats, conversion rate, auto-refresh
This commit is contained in:
2026-03-03 03:47:18 +08:00
parent 1389c848b2
commit 0236867c88
32 changed files with 2293 additions and 494 deletions

View File

@@ -51,6 +51,9 @@ export async function GET(
scanCount: s.scanCount,
pledgeCount: s._count.pledges,
totalPledged: s.pledges.reduce((sum: number, p: QrPledge) => sum + p.amountPence, 0),
totalCollected: s.pledges
.filter((p: QrPledge) => p.status === "paid")
.reduce((sum: number, p: QrPledge) => sum + p.amountPence, 0),
createdAt: s.createdAt,
}))
)

View File

@@ -0,0 +1,80 @@
import { NextResponse } from "next/server"
import prisma from "@/lib/prisma"
export async function GET(
_req: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params
if (!prisma) {
return NextResponse.json({ error: "Database not configured" }, { status: 503 })
}
// Find event by slug (try both org-scoped and plain slug)
const event = await prisma.event.findFirst({
where: {
OR: [
{ slug },
{ slug: { contains: slug } },
],
status: { in: ["active", "closed"] },
},
include: {
organization: { select: { name: true } },
qrSources: {
select: { code: true, label: true, volunteerName: true },
orderBy: { createdAt: "asc" },
},
pledges: {
select: {
donorName: true,
amountPence: true,
status: true,
giftAid: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
},
},
})
if (!event) {
return NextResponse.json({ error: "Event not found" }, { status: 404 })
}
const pledges = event.pledges
const totalPledged = pledges.reduce((s, p) => s + p.amountPence, 0)
const totalPaid = pledges
.filter((p) => p.status === "paid")
.reduce((s, p) => s + p.amountPence, 0)
const giftAidCount = pledges.filter((p) => p.giftAid).length
return NextResponse.json({
id: event.id,
name: event.name,
description: event.description,
eventDate: event.eventDate,
location: event.location,
goalAmount: event.goalAmount,
organizationName: event.organization.name,
stats: {
pledgeCount: pledges.length,
totalPledged,
totalPaid,
giftAidCount,
avgPledge: pledges.length > 0 ? Math.round(totalPledged / pledges.length) : 0,
},
recentPledges: pledges.slice(0, 10).map((p) => ({
donorName: p.donorName ? p.donorName.split(" ")[0] + " " + (p.donorName.split(" ")[1]?.[0] || "") + "." : null,
amountPence: p.amountPence,
createdAt: p.createdAt,
giftAid: p.giftAid,
})),
qrCodes: event.qrSources,
})
} catch (error) {
console.error("Public event error:", error)
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}