From 8a8c621532b8c7f8784912d56a6fa81724c1f25c Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 19 Mar 2026 15:35:37 -0700 Subject: [PATCH] Move registration form to root path, remove /register Registration page now lives at / instead of /register. Updated all links and embed snippet accordingly. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 +- app/page.tsx | 585 +++++++++++++++++++++++++++++++++++++++++- app/register/page.tsx | 584 ----------------------------------------- embed-snippet.html | 4 +- lib/event.config.ts | 2 +- 5 files changed, 587 insertions(+), 592 deletions(-) delete mode 100644 app/register/page.tsx diff --git a/CLAUDE.md b/CLAUDE.md index accdc8d..b04f3fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Adapted from `crypto-commons-gather.ing-website` with centralized config. - **Key pattern**: All event-specific config centralized in `lib/event.config.ts` ## Flow -1. User clicks "Register" button on `www.collaborative-finance.net` → opens `register.collaborative-finance.net/register` +1. User clicks "Register" button on `www.collaborative-finance.net` → opens `register.collaborative-finance.net` 2. User fills form → POST `/api/register` → Google Sheets (Pending) 3. User picks accommodation/payment → POST `/api/create-checkout-session` → Mollie redirect 4. Mollie webhook POST `/api/webhook` → verify payment → assign booking → update sheet → email → Listmonk @@ -32,7 +32,7 @@ that links to the registration page. No iframe, no JS, no CORS — just a simple ## Dev Workflow ```bash pnpm install -pnpm dev # localhost:3000 → redirects to /register +pnpm dev # localhost:3000 → registration form ``` ## Deployment diff --git a/app/page.tsx b/app/page.tsx index 2b74e9f..63613ea 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,584 @@ -import { redirect } from "next/navigation" +"use client" -export default function Home() { - redirect("/register") +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 { + EVENT_SHORT, + EVENT_FULL_NAME, + EVENT_DATES, + EVENT_LOCATION, + PRICING_TIERS, + PROCESSING_FEE_PERCENT, + ACCOMMODATION_VENUES, + ACCOMMODATION_MAP, + ACCOMMODATION_NIGHTS, + LINKS, +} from "@/lib/event.config" + +// Determine current tier client-side +function getClientTier() { + const now = new Date().toISOString().slice(0, 10) + return PRICING_TIERS.find((t) => now < t.cutoff) ?? PRICING_TIERS[PRICING_TIERS.length - 1] +} + +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 [selectedVenueKey, setSelectedVenueKey] = useState(ACCOMMODATION_VENUES[0]?.key || "") + const [accommodationType, setAccommodationType] = useState( + ACCOMMODATION_VENUES[0]?.options[0]?.id || "" + ) + const [formData, setFormData] = useState({ + name: "", + email: "", + contact: "", + contributions: "", + expectations: "", + howHeard: "", + dietary: [] as string[], + dietaryOther: "", + crewConsent: "", + }) + + const tier = getClientTier() + const baseTicketPrice = tier.price + + const accommodationPrice = ACCOMMODATION_MAP[accommodationType]?.price ?? 0 + const subtotalPrice = + baseTicketPrice + + (includeAccommodation ? accommodationPrice : 0) + const processingFee = Math.round(subtotalPrice * PROCESSING_FEE_PERCENT * 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), + })) + } + + // Pricing summary line + const pricingSummary = PRICING_TIERS.map( + (t) => `€${t.price} ${t.label}${t === tier ? " (current)" : ""}` + ).join(" · ") + + if (step === "payment") { + return ( +
+ {/* Header */} +
+
+ + {EVENT_SHORT} + +
+
+ +
+
+

Complete Your Registration

+

Choose your payment method

+
+ + + + Event Registration + + {pricingSummary} + + + +
+ {/* Ticket */} +
+
+
{EVENT_SHORT} Ticket
+
+ {pricingSummary} +
+
+ €{baseTicketPrice.toFixed(2)} +
+ + {/* Accommodation */} +
+
+ setIncludeAccommodation(checked as boolean)} + className="mt-1" + /> +
+
+ + {includeAccommodation && ( + €{accommodationPrice.toFixed(2)} + )} +
+ {includeAccommodation ? ( +
+ {/* Venue selection */} + { + setSelectedVenueKey(value) + const venue = ACCOMMODATION_VENUES.find((v) => v.key === value) + if (venue?.options[0]) { + setAccommodationType(venue.options[0].id) + } + }} + className="space-y-2" + > + {ACCOMMODATION_VENUES.map((venue) => ( +
+ + +
+ ))} +
+ + {/* Sub-options for selected venue */} + {ACCOMMODATION_VENUES.filter((v) => v.key === selectedVenueKey).map((venue) => ( +
+

+ {venue.description} +

+ + {venue.options.map((opt) => ( +
+ + +
+ ))} +
+
+ ))} +
+ ) : ( +

+ 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" + /> +
+ +

+ 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 ({(PROCESSING_FEE_PERCENT * 100).toFixed(0)}%)
+
+ €{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. +

+
+
+
+ + {/* Simplified Footer */} + +
+ ) + } + + return ( +
+ {/* Header */} +
+
+ + {EVENT_SHORT} + +
+
+ +
+
+

Register for {EVENT_SHORT}

+

{EVENT_DATES} · {EVENT_LOCATION}

+
+ + + + Registration Form + Tell us about yourself and what you'd like to bring to {EVENT_SHORT} + + +
+ {/* 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 */} +
+ +