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"
+ />
+
+
+
+ Accommodation ({ACCOMMODATION_NIGHTS} nights)
+
+ {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) => (
+
+
+
+ {venue.name}
+
+
+ ))}
+
+
+ {/* Sub-options for selected venue */}
+ {ACCOMMODATION_VENUES.filter((v) => v.key === selectedVenueKey).map((venue) => (
+
+
+ {venue.description}
+
+
+ {venue.options.map((opt) => (
+
+
+
+ {opt.label} — €{opt.price.toFixed(2)} (€{opt.nightlyRate}/night)
+
+
+ ))}
+
+
+ ))}
+
+ ) : (
+
+ 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"
+ />
+
+
+ I would like to include food
+
+
+ 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
+
+
+
+
+
+
+
+
+ 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}
+
+
+
+
+
+
+
+ {/* Simplified Footer */}
+
+
+ )
}
diff --git a/app/register/page.tsx b/app/register/page.tsx
deleted file mode 100644
index 63613ea..0000000
--- a/app/register/page.tsx
+++ /dev/null
@@ -1,584 +0,0 @@
-"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 {
- 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"
- />
-
-
-
- Accommodation ({ACCOMMODATION_NIGHTS} nights)
-
- {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) => (
-
-
-
- {venue.name}
-
-
- ))}
-
-
- {/* Sub-options for selected venue */}
- {ACCOMMODATION_VENUES.filter((v) => v.key === selectedVenueKey).map((venue) => (
-
-
- {venue.description}
-
-
- {venue.options.map((opt) => (
-
-
-
- {opt.label} — €{opt.price.toFixed(2)} (€{opt.nightlyRate}/night)
-
-
- ))}
-
-
- ))}
-
- ) : (
-
- 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"
- />
-
-
- I would like to include food
-
-
- 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.
-
-
-
- setStep("form")} className="flex-1">
- Back to Form
-
-
- Proceed to Payment
-
-
-
-
-
-
-
-
-
- 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 */}
-
- What's your name? *
- setFormData({ ...formData, name: e.target.value })}
- />
-
-
- {/* Email */}
-
-
Email address *
-
setFormData({ ...formData, email: e.target.value })}
- />
-
- We'll send your registration confirmation and event updates here.
-
-
-
- {/* Contact */}
-
- How can we contact you besides via email? *
- setFormData({ ...formData, contact: e.target.value })}
- />
-
-
- {/* Contributions */}
-
-
What do you want to contribute to {EVENT_SHORT}? *
-
setFormData({ ...formData, contributions: e.target.value })}
- />
-
- This event is participant-driven — each attendee is invited to co-create the program.
-
-
-
- {/* Expectations */}
-
- What do you expect to gain from participating? *
- setFormData({ ...formData, expectations: e.target.value })}
- />
-
-
- {/* How heard */}
-
- How did you hear about {EVENT_SHORT}?
- setFormData({ ...formData, howHeard: e.target.value })}
- />
-
-
- {/* Dietary Requirements */}
-
-
Dietary Requirements
-
- Do you have any special dietary requirements?
-
-
- {["vegetarian", "vegan", "gluten free", "lactose free"].map((diet) => (
-
- handleDietaryChange(diet, checked as boolean)}
- />
-
- {diet.charAt(0).toUpperCase() + diet.slice(1)}
-
-
- ))}
-
-
-
-
- {/* Participation Consent */}
-
-
Participation Commitment *
-
-
- {EVENT_SHORT} is a collaborative event — participants co-create its program,
- activities, and atmosphere together.
-
-
- You will be expected to actively contribute to the event beyond just attending sessions.
-
-
-
setFormData({ ...formData, crewConsent: value })}
- >
-
-
-
- Can't wait
-
-
-
-
-
- Yes please, sign me up!
-
-
-
-
-
- I love getting my hands dirty :D
-
-
-
-
-
-
-
- {isSubmitting ? "Recording registration..." : "Continue to Payment"}
-
-
-
-
-
-
-
-
-
- {/* Simplified Footer */}
-
-
- )
-}
diff --git a/embed-snippet.html b/embed-snippet.html
index de8d5c5..feb846d 100644
--- a/embed-snippet.html
+++ b/embed-snippet.html
@@ -9,7 +9,7 @@
+
Register for CoFi 2026
-->
diff --git a/lib/event.config.ts b/lib/event.config.ts
index 69e2600..70739dd 100644
--- a/lib/event.config.ts
+++ b/lib/event.config.ts
@@ -179,7 +179,7 @@ export const EMAIL_BRANDING = {
export const LINKS = {
website: "https://www.collaborative-finance.net",
- register: "https://register.collaborative-finance.net/register",
+ register: "https://register.collaborative-finance.net",
telegram: "", // TBD — set when community channel is created
community: "", // TBD — set when community channel is created
contactEmail: "cofi.gathering@gmail.com",