feat: add optional packages to registration flow

Update registration to include dynamic package selection and total.

#VERCEL_SKIP

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
This commit is contained in:
v0 2025-12-07 20:47:36 +00:00
parent 747204b27d
commit c17694b9b0
2 changed files with 98 additions and 47 deletions

View File

@ -5,14 +5,41 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
})
const CCG_TICKET_PRICE_ID = "price_1SbokZ8IwXvKSVJpRvkTqePT"
const CCG_ACCOMMODATION_PRICE_ID = "price_1Sboq08IwXvKSVJpf8RRSoCy"
const CCG_FOOD_PRICE_ID = "price_1Sboq18IwXvKSVJpY7NJWaYd"
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const paymentMethod = formData.get("paymentMethod") as string
const registrationDataStr = formData.get("registrationData") as string
const packagesStr = formData.get("packages") as string
// Parse registration data
const registrationData = registrationDataStr ? JSON.parse(registrationDataStr) : null
const packages = packagesStr ? JSON.parse(packagesStr) : { accommodation: false, food: false }
const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [
{
price: CCG_TICKET_PRICE_ID,
quantity: 1,
},
]
if (packages.accommodation) {
lineItems.push({
price: CCG_ACCOMMODATION_PRICE_ID,
quantity: 1,
})
}
if (packages.food) {
lineItems.push({
price: CCG_FOOD_PRICE_ID,
quantity: 1,
})
}
// Configure payment method types based on selection
let paymentMethodTypes: Stripe.Checkout.SessionCreateParams.PaymentMethodType[] = ["card"]
@ -25,25 +52,7 @@ export async function POST(request: NextRequest) {
const session = await stripe.checkout.sessions.create({
payment_method_types: paymentMethodTypes,
line_items: [
{
price_data: {
currency: "eur",
product_data: {
name: "Crypto Commons Gathering 2026",
description: "Full event access including all sessions and infrastructure",
images: ["https://ccg2025.vercel.app/og-image.png"],
},
unit_amount: 20000, // €200
},
quantity: 1,
adjustable_quantity: {
enabled: true,
minimum: 1,
maximum: 5,
},
},
],
line_items: lineItems,
mode: "payment",
success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${request.nextUrl.origin}/register`,
@ -51,13 +60,15 @@ export async function POST(request: NextRequest) {
? {
name: registrationData.name,
contact: registrationData.contact,
contributions: registrationData.contributions.substring(0, 500), // Stripe has length limits
contributions: registrationData.contributions.substring(0, 500),
expectations: registrationData.expectations.substring(0, 500),
howHeard: registrationData.howHeard || "",
dietary:
registrationData.dietary.join(", ") +
(registrationData.dietaryOther ? `, ${registrationData.dietaryOther}` : ""),
crewConsent: registrationData.crewConsent,
includesAccommodation: packages.accommodation ? "yes" : "no",
includesFood: packages.food ? "yes" : "no",
}
: {},
// For stablecoin payments

View File

@ -24,6 +24,20 @@ export default function RegisterPage() {
dietaryOther: "",
crewConsent: "",
})
const [packages, setPackages] = useState({
accommodation: false,
food: false,
})
const baseTicketPrice = 200
const accommodationPrice = 227.4
const foodPrice = 135
const calculateTotal = () => {
let total = baseTicketPrice
if (packages.accommodation) total += accommodationPrice
if (packages.food) total += foodPrice
return total
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
@ -63,63 +77,89 @@ export default function RegisterPage() {
<main className="container mx-auto px-4 py-12 max-w-5xl">
<div className="text-center mb-12">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Complete Your Registration</h1>
<p className="text-xl text-muted-foreground">Choose your payment method</p>
<p className="text-xl text-muted-foreground">Select packages and payment method</p>
</div>
<Card className="mb-8 border-primary/40">
<CardHeader>
<CardTitle>Event Cost Breakdown</CardTitle>
<CardDescription>Full 6-day event costs (August 16-22, 2026)</CardDescription>
<CardTitle>Select Your Packages</CardTitle>
<CardDescription>Ticket is required. Add accommodation and food as needed.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between items-center py-3 border-b border-border">
<div>
<div className="font-medium">Ticket Price</div>
<div className="text-sm text-muted-foreground">Venue rental & infrastructure</div>
<div className="space-y-4">
{/* Ticket (required) */}
<div className="flex items-start justify-between py-4 border-b border-border">
<div className="flex items-start gap-3">
<Checkbox checked disabled className="mt-1" />
<div>
<div className="font-medium">CCG 2026 Ticket (Required)</div>
<div className="text-sm text-muted-foreground">Venue rental & infrastructure</div>
</div>
</div>
<span className="text-lg font-semibold">200</span>
<span className="text-lg font-semibold">200.00</span>
</div>
<div className="flex justify-between items-center py-3 border-b border-border">
<div>
<div className="font-medium">Accommodation</div>
<div className="text-sm text-muted-foreground">6 nights dorm (37.90/night)</div>
{/* Accommodation */}
<div className="flex items-start justify-between py-4 border-b border-border">
<div className="flex items-start gap-3">
<Checkbox
id="accommodation"
checked={packages.accommodation}
onCheckedChange={(checked) => setPackages({ ...packages, accommodation: checked as boolean })}
className="mt-1"
/>
<Label htmlFor="accommodation" className="cursor-pointer">
<div className="font-medium">Accommodation (Optional)</div>
<div className="text-sm text-muted-foreground">6 nights dorm at Commons Hub (37.90/night)</div>
</Label>
</div>
<span className="text-lg font-semibold">227.40</span>
</div>
<div className="flex justify-between items-center py-3 border-b border-border">
<div>
<div className="font-medium">Food</div>
<div className="text-sm text-muted-foreground">6 days meals (22.50/day avg)</div>
{/* Food */}
<div className="flex items-start justify-between py-4 border-b border-border">
<div className="flex items-start gap-3">
<Checkbox
id="food"
checked={packages.food}
onCheckedChange={(checked) => setPackages({ ...packages, food: checked as boolean })}
className="mt-1"
/>
<Label htmlFor="food" className="cursor-pointer">
<div className="font-medium">Food Package (Optional)</div>
<div className="text-sm text-muted-foreground">6 days of meals (22.50/day avg)</div>
</Label>
</div>
<span className="text-lg font-semibold">135</span>
<span className="text-lg font-semibold">135.00</span>
</div>
{/* Total */}
<div className="flex justify-between items-center py-4 bg-primary/10 -mx-6 px-6 mt-4">
<div>
<div className="font-bold text-lg">Total Estimated Cost</div>
<div className="text-sm text-muted-foreground">Pay ticket now, accommodation & food at venue</div>
<div className="font-bold text-lg">Total Amount</div>
<div className="text-sm text-muted-foreground">
{packages.accommodation || packages.food
? "All selected items will be charged now"
: "Ticket only"}
</div>
</div>
<span className="text-2xl font-bold text-primary">562.40</span>
<span className="text-2xl font-bold text-primary">{calculateTotal().toFixed(2)}</span>
</div>
</div>
<p className="text-sm text-muted-foreground mt-4">
You're only paying the ticket price (200) now. Accommodation and food will be arranged and paid
separately at the Commons Hub.
</p>
</CardContent>
</Card>
{/* </CHANGE> */}
{/* Payment Methods */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Payment Options</CardTitle>
<CardDescription>Ticket Price: 200 per person</CardDescription>
<CardDescription>Choose your preferred payment method</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form action="/api/create-checkout-session" method="POST">
<input type="hidden" name="registrationData" value={JSON.stringify(formData)} />
<input type="hidden" name="packages" value={JSON.stringify(packages)} />
<div className="space-y-4">
<div>