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:
parent
747204b27d
commit
c17694b9b0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue