110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { auth } from "@/auth";
|
|
import { isAdminRole } from "@/lib/admin";
|
|
import { creditDisplayName } from "@/lib/credits-brand";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { debitWalletCredits, INSUFFICIENT_CREDITS } from "@/lib/wallet-safety";
|
|
|
|
const ISSUE_CATALOG = [
|
|
{ slug: "voting-rights", title: "Voting Rights & Access" },
|
|
{ slug: "healthcare", title: "Affordable Healthcare" },
|
|
{ slug: "climate", title: "Climate & Clean Energy Jobs" },
|
|
{ slug: "education", title: "Public Education Funding" },
|
|
{ slug: "gun-safety", title: "Common-Sense Gun Safety" },
|
|
{ slug: "workers-rights", title: "Workers Rights & Wages" },
|
|
{ slug: "housing", title: "Affordable Housing" },
|
|
{ slug: "democracy-reform", title: "Campaign Finance Reform" },
|
|
] as const;
|
|
|
|
function currentWeekOf(): string {
|
|
const d = new Date();
|
|
const day = d.getUTCDay();
|
|
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
|
|
const mon = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), diff));
|
|
return mon.toISOString().split("T")[0];
|
|
}
|
|
|
|
const postSchema = z.object({
|
|
issueSlug: z.string().min(2).max(80),
|
|
creditsSpent: z.number().int().min(5).max(10000),
|
|
});
|
|
|
|
export async function GET() {
|
|
const weekOf = currentWeekOf();
|
|
const bids = await prisma.spotlightBid.groupBy({
|
|
by: ["issueSlug", "issueTitle"],
|
|
where: { weekOf },
|
|
_sum: { creditsSpent: true },
|
|
_count: true,
|
|
orderBy: { _sum: { creditsSpent: "desc" } },
|
|
});
|
|
|
|
const totals = bids.map((b) => ({
|
|
issueSlug: b.issueSlug,
|
|
issueTitle: b.issueTitle,
|
|
total: b._sum.creditsSpent ?? 0,
|
|
backers: b._count,
|
|
}));
|
|
|
|
const leader = totals[0] ?? null;
|
|
|
|
return NextResponse.json({ weekOf, totals, leader, catalog: ISSUE_CATALOG });
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "Sign in to bid on a Spotlight." }, { status: 401 });
|
|
}
|
|
|
|
const admin = isAdminRole(session.user.role);
|
|
const creditName = creditDisplayName();
|
|
|
|
try {
|
|
const json = await req.json();
|
|
const { issueSlug, creditsSpent } = postSchema.parse(json);
|
|
|
|
const issue = ISSUE_CATALOG.find((i) => i.slug === issueSlug);
|
|
if (!issue) return NextResponse.json({ error: "Unknown issue." }, { status: 400 });
|
|
|
|
const weekOf = currentWeekOf();
|
|
const spent = admin ? 0 : creditsSpent;
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
const bid = await tx.spotlightBid.create({
|
|
data: {
|
|
userId: session.user.id,
|
|
issueSlug: issue.slug,
|
|
issueTitle: issue.title,
|
|
creditsSpent: spent,
|
|
weekOf,
|
|
},
|
|
});
|
|
|
|
if (!admin) {
|
|
await debitWalletCredits(tx, session.user.id, creditsSpent);
|
|
|
|
await tx.ledgerEntry.create({
|
|
data: {
|
|
userId: session.user.id,
|
|
delta: -creditsSpent,
|
|
type: "DEBIT_SPOTLIGHT",
|
|
spotlightBidId: bid.id,
|
|
memo: `Spotlight bid: ${issue.title}`,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
return NextResponse.json({ ok: true, issue, weekOf, spent });
|
|
} catch (e) {
|
|
if (e instanceof z.ZodError) return NextResponse.json({ error: "Invalid input", issues: e.issues }, { status: 400 });
|
|
if (e instanceof Error && e.message === INSUFFICIENT_CREDITS) {
|
|
return NextResponse.json({ error: `Not enough ${creditName}.` }, { status: 402 });
|
|
}
|
|
console.error(e);
|
|
return NextResponse.json({ error: "Could not place bid." }, { status: 500 });
|
|
}
|
|
}
|