From b02ebcacae327967fb5cfad87b0c8a134aaf642e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 2 Apr 2026 16:20:45 -0700 Subject: [PATCH] feat: add secret early bird pricing link at /register/friends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts registration form into shared component and adds promo code support so late registrants can be offered early bird pricing (€80) via a private URL, using the same Mollie/Sheet/booking pipeline. Co-Authored-By: Claude Opus 4.6 --- app/api/create-checkout-session/route.ts | 18 +- app/register/friends/page.tsx | 22 + app/register/page.tsx | 817 +--------------------- components/register-form.tsx | 845 +++++++++++++++++++++++ 4 files changed, 885 insertions(+), 817 deletions(-) create mode 100644 app/register/friends/page.tsx create mode 100644 components/register-form.tsx diff --git a/app/api/create-checkout-session/route.ts b/app/api/create-checkout-session/route.ts index 6950d59..c6e556c 100644 --- a/app/api/create-checkout-session/route.ts +++ b/app/api/create-checkout-session/route.ts @@ -18,12 +18,24 @@ const PRICING_TIERS = [ { label: "Late", price: 150, cutoff: "2099-12-31" }, ] +// Promo codes — map code → tier label +const PROMO_CODES: Record = { + "earlybird-friends": "Early bird", +} + function getCurrentTicketPrice(): number { const now = new Date().toISOString().slice(0, 10) const tier = PRICING_TIERS.find((t) => now < t.cutoff) ?? PRICING_TIERS[PRICING_TIERS.length - 1] return tier.price } +function getTicketPriceForPromo(code: string): number | null { + const tierLabel = PROMO_CODES[code] + if (!tierLabel) return null + const tier = PRICING_TIERS.find((t) => t.label === tierLabel) + return tier?.price ?? null +} + const PROCESSING_FEE_PERCENT = 0.02 // 2% to cover Mollie payment processing fees // Accommodation prices per person for 7 nights @@ -49,8 +61,10 @@ export async function POST(request: NextRequest) { const registrationData = registrationDataStr ? JSON.parse(registrationDataStr) : null - // Calculate subtotal - const ticketPrice = getCurrentTicketPrice() + // Calculate subtotal — check for valid promo code override + const promoCode = (formData.get("promoCode") as string) || "" + const promoPrice = promoCode ? getTicketPriceForPromo(promoCode) : null + const ticketPrice = promoPrice ?? getCurrentTicketPrice() let subtotal = ticketPrice const descriptionParts = [`CCG 2026 Ticket (€${ticketPrice})`] diff --git a/app/register/friends/page.tsx b/app/register/friends/page.tsx new file mode 100644 index 0000000..dfbc748 --- /dev/null +++ b/app/register/friends/page.tsx @@ -0,0 +1,22 @@ +"use client" + +import RegisterForm, { PRICING_TIERS } from "@/components/register-form" + +const EARLY_BIRD_TIER = PRICING_TIERS[0] +const PROMO_CODE = "earlybird-friends" + +export default function RegisterFriendsPage() { + return ( + +

+ Special early bird pricing — €{EARLY_BIRD_TIER.price} ticket +

+ + } + /> + ) +} diff --git a/app/register/page.tsx b/app/register/page.tsx index 3608f8e..dbfbad5 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,820 +1,7 @@ "use client" -import type React from "react" - -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Checkbox } from "@/components/ui/checkbox" -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" -import Link from "next/link" -import { useState } from "react" +import RegisterForm from "@/components/register-form" export default function RegisterPage() { - const [step, setStep] = useState<"form" | "payment">("form") - const [isSubmitting, setIsSubmitting] = useState(false) - const [includeAccommodation, setIncludeAccommodation] = useState(true) - const [wantFood, setWantFood] = useState(false) - const [accommodationVenue, setAccommodationVenue] = useState<"commons-hub" | "herrnhof">("commons-hub") - const [accommodationType, setAccommodationType] = useState("ch-multi") - const [formData, setFormData] = useState({ - name: "", - email: "", - contact: "", - contributions: "", - expectations: "", - howHeard: "", - dietary: [] as string[], - dietaryOther: "", - crewConsent: "", - }) - // Dynamic pricing tiers - const pricingTiers = [ - { label: "Early bird", price: 80, cutoff: "2026-03-31" }, - { label: "Regular", price: 120, cutoff: "2026-07-01" }, - { label: "Late", price: 150, cutoff: "2099-12-31" }, - ] - const now = new Date().toISOString().slice(0, 10) - const currentTier = pricingTiers.find((t) => now < t.cutoff) ?? pricingTiers[pricingTiers.length - 1] - const baseTicketPrice = currentTier.price - - const accommodationPrices: Record = { - "ch-multi": { label: "Bed in shared room (Commons Hub)", price: 279.30 }, - "ch-double": { label: "Bed in double room (Commons Hub)", price: 356.30 }, - "hh-single": { label: "Single room (Herrnhof)", price: 665 }, - "hh-double-separate": { label: "Double room, separate beds (Herrnhof)", price: 420 }, - "hh-double-shared": { label: "Double room, shared double bed (Herrnhof)", price: 350 }, - "hh-triple": { label: "Triple room, one single + one shared double bed (Herrnhof)", price: 350 }, - "hh-daybed": { label: "Daybed or extra bed in living room (Herrnhof)", price: 280 }, - } - - const accommodationPrice = accommodationPrices[accommodationType]?.price ?? 0 - const subtotalPrice = - baseTicketPrice + - (includeAccommodation ? accommodationPrice : 0) - const processingFee = Math.round(subtotalPrice * 0.02 * 100) / 100 - const totalPrice = subtotalPrice + processingFee - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - // Validate required fields - if ( - !formData.name || - !formData.email || - !formData.contact || - !formData.contributions || - !formData.expectations || - !formData.crewConsent - ) { - alert("Please fill in all required fields") - return - } - - setIsSubmitting(true) - - try { - // Submit registration to Google Sheet first - const dietaryString = - formData.dietary.join(", ") + - (formData.dietaryOther ? `, ${formData.dietaryOther}` : "") - - const response = await fetch("/api/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: formData.name, - email: formData.email, - contact: formData.contact, - contributions: formData.contributions, - expectations: formData.expectations, - howHeard: formData.howHeard, - dietary: dietaryString, - crewConsent: formData.crewConsent, - wantFood, - }), - }) - - if (!response.ok) { - throw new Error("Failed to record registration") - } - - // Proceed to payment step - setStep("payment") - } catch (error) { - console.error("Registration error:", error) - alert("There was an error recording your registration. Please try again.") - } finally { - setIsSubmitting(false) - } - } - - const handleDietaryChange = (value: string, checked: boolean) => { - setFormData((prev) => ({ - ...prev, - dietary: checked ? [...prev.dietary, value] : prev.dietary.filter((item) => item !== value), - })) - } - - if (step === "payment") { - return ( -
- {/* Header */} -
-
- - CCG - -
-
- -
-
-

Complete Your Registration

-

Choose your payment method

-
- - - - Event Registration - - {currentTier.label} pricing — €{currentTier.price} - - - -
- {/* Ticket */} -
-
-
CCG 2026 Ticket
-
- {pricingTiers.map((t, i) => ( - {i > 0 ? " · " : ""}€{t.price} {t.label}{t === currentTier ? " (current)" : ""} - ))} -
-
- CCA members: Bring two newcomers, get a free ticket! -
-
- €{baseTicketPrice.toFixed(2)} -
- - {/* Accommodation */} -
-
- setIncludeAccommodation(checked as boolean)} - className="mt-1" - /> -
-
- - {includeAccommodation && ( - €{accommodationPrice.toFixed(2)} - )} -
- {includeAccommodation ? ( -
- {/* Venue selection */} - { - const venue = value as "commons-hub" | "herrnhof" - setAccommodationVenue(venue) - setAccommodationType(venue === "commons-hub" ? "ch-multi" : "hh-single") - }} - className="space-y-2" - > -
- - -
-
- - -
-
- - {/* Sub-options per venue */} - {accommodationVenue === "commons-hub" ? ( -
-

- 30 beds on-site at the main venue. Basic, communal accommodation. -

- -
- - -
-
- - -
-
-
- ) : ( -
-

- Historic 19th-century mansion with newly renovated apartments, sauna, - and river swimming platform. Prices per person for 7 nights (incl. city tax & VAT). -

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-

- For whole-apartment quotes, contact{" "} - office@commons-hub.at. -

-
- )} -
- ) : ( -

- I'll arrange my own accommodation -

- )} -
-
-
- - {includeAccommodation && ( -

- We'll follow up closer to the event to confirm room assignments and accommodation details. -

- )} - - {/* Food */} -
-
- setWantFood(checked as boolean)} - className="mt-1" - /> -
- -

- We are exploring co-producing our own meals as a community. More details and costs - will be shared soon — checking this box registers your interest so we can plan accordingly. - Your dietary preferences from step 1 have been noted. -

-
-
-
- - {/* Processing fee */} -
-
-
Payment processing fee (2%)
-
- €{processingFee.toFixed(2)} -
- - {/* Total */} -
-
-
Total Amount
-
- Ticket{includeAccommodation ? " + accommodation" : ""} -
-
- €{totalPrice.toFixed(2)} -
-
-
-
- - {/* Payment Methods */} -
- - - Payment Options - Choose your preferred payment method - - -
- - - - -
-

- You'll be redirected to Mollie's secure checkout where you can pay by credit card, - SEPA bank transfer, iDEAL, PayPal, or other methods. -

- -
- - -
-
-
-
-
- -
-

- All payments are processed securely through Mollie. You'll receive a confirmation email after successful - payment. -

-
-
-
- - {/* Footer */} -
-
-
-
-

CCG 2026

-

- Crypto Commons Gathering -
- August 16-23, 2026 -

-
- -
-

Links

-
    -
  • - - Register to Attend - -
  • -
  • - - Gallery - -
  • -
  • - - About CCG 2026 - -
  • -
  • - - Directions - -
  • -
  • - - Financial Transparency - -
  • -
-
- -
-

Community

-
    -
  • - - Join the CCG26 Telegram - -
  • -
  • - - Join the Crypto Commons Association Telegram - -
  • -
-
- -
-

Partners

-
    -
  • - - Commons Hub - -
  • -
  • - - Crypto Commons Association - -
  • -
  • - - Sponsorships - -
  • -
-
-
- -
-

This website is under Creative Commons license. Built with solidarity for the commons.

-
-
-
-
- ) - } - - return ( -
- {/* Header */} -
-
- - CCG - -
-
- -
-
-

Register for CCG 2026

-

August 16-23, 2026 at the Commons Hub in Austria

-
- - - - Registration Form - Tell us about yourself and what you'd like to bring to CCG - - -
- {/* Name */} -
- - setFormData({ ...formData, name: e.target.value })} - /> -
- - {/* Email */} -
- - setFormData({ ...formData, email: e.target.value })} - /> -

- We'll send your registration confirmation and event updates here. -

-
- - {/* Contact */} -
- - setFormData({ ...formData, contact: e.target.value })} - /> -
- - {/* Contributions */} -
- -