crypto-commons-gather.ing-w.../components/register-form.tsx

794 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { useEffect, useState } from "react"
// ── Pricing & accommodation constants ─────────────────────────
const PRICING_TIERS = [
{ 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" },
]
function getCurrentTier() {
const now = new Date().toISOString().slice(0, 10)
return PRICING_TIERS.find((t) => now < t.cutoff) ?? PRICING_TIERS[PRICING_TIERS.length - 1]
}
const ACCOMMODATION_PRICES: Record<string, { label: string; price: number }> = {
"ch-multi": { label: "Bed in shared room (Commons Hub)", price: 279.30 },
"ch-double": { label: "Bed in double room (Commons Hub)", price: 356.30 },
}
// ── Promo codes ───────────────────────────────────────────────
export const PROMO_CODES: Record<string, string> = {
"earlybird-friends": "Early bird",
}
export { PRICING_TIERS }
// ── Component ─────────────────────────────────────────────────
interface RegisterFormProps {
tierOverride?: { label: string; price: number; cutoff: string }
promoCode?: string
banner?: React.ReactNode
}
export default function RegisterForm({ tierOverride, promoCode, banner }: RegisterFormProps) {
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">("commons-hub")
const [accommodationType, setAccommodationType] = useState("ch-multi")
const [formData, setFormData] = useState({
name: "",
email: "",
contact: "",
contributions: "",
expectations: "",
howHeard: "",
dietary: [] as string[],
dietaryOther: "",
crewConsent: "",
})
const [availability, setAvailability] = useState<Record<string, boolean>>({})
// Fetch accommodation availability on mount
useEffect(() => {
fetch("/api/accommodation-availability")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data && typeof data === "object" && !data.error) {
setAvailability(data)
// If current selection is sold out, switch to first available
if (data[accommodationType] === false) {
const firstAvailable = Object.keys(ACCOMMODATION_PRICES).find((k) => data[k] !== false)
if (firstAvailable) setAccommodationType(firstAvailable)
}
}
})
.catch(() => {})
}, []) // eslint-disable-line react-hooks/exhaustive-deps
const currentTier = tierOverride ?? getCurrentTier()
const baseTicketPrice = currentTier.price
const accommodationPrice = ACCOMMODATION_PRICES[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 (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b">
<div className="container mx-auto px-4 py-4">
<Link href="/" className="text-2xl font-bold text-primary">
CCG
</Link>
</div>
</header>
<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>
</div>
{banner}
<Card className="mb-8 border-primary/40">
<CardHeader>
<CardTitle>Event Registration</CardTitle>
<CardDescription>
{currentTier.label} pricing {currentTier.price}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Ticket */}
<div className="flex items-start justify-between py-4 border-b border-border">
<div>
<div className="font-medium">CCG 2026 Ticket</div>
<div className="text-sm text-muted-foreground">
{PRICING_TIERS.map((t, i) => (
<span key={t.label}>{i > 0 ? " · " : ""}{t.price} {t.label}{t.label === currentTier.label ? " (current)" : ""}</span>
))}
</div>
<div className="text-xs text-muted-foreground mt-1">
CCA members: Bring two newcomers, get a free ticket!
</div>
</div>
<span className="text-lg font-semibold whitespace-nowrap ml-4">{baseTicketPrice.toFixed(2)}</span>
</div>
{/* Accommodation */}
<div className="py-4 border-b border-border">
<div className="flex items-start gap-3">
<Checkbox
id="include-accommodation"
checked={includeAccommodation}
onCheckedChange={(checked) => setIncludeAccommodation(checked as boolean)}
className="mt-1"
/>
<div className="flex-1">
<div className="flex justify-between items-start">
<Label htmlFor="include-accommodation" className="font-medium cursor-pointer">
Accommodation (7 nights, Aug 1623)
</Label>
{includeAccommodation && (
<span className="text-lg font-semibold whitespace-nowrap ml-4">{accommodationPrice.toFixed(2)}</span>
)}
</div>
{includeAccommodation ? (
<div className="mt-3 space-y-4">
<div className="pl-6 border-l-2 border-primary/20">
<p className="text-xs text-muted-foreground mb-2">
30 beds on-site at the main venue. Basic, communal accommodation.
</p>
<RadioGroup
value={accommodationType}
onValueChange={setAccommodationType}
className="space-y-2"
>
<div className={`flex items-center space-x-2 ${availability["ch-multi"] === false ? "opacity-50" : ""}`}>
<RadioGroupItem value="ch-multi" id="ch-multi" disabled={availability["ch-multi"] === false} />
<Label htmlFor="ch-multi" className={`font-normal text-sm ${availability["ch-multi"] === false ? "cursor-not-allowed" : "cursor-pointer"}`}>
Bed in shared room {availability["ch-multi"] === false ? (
<span className="text-destructive font-medium">Sold Out</span>
) : (
<>279.30 (39.90/night)</>
)}
</Label>
</div>
<div className={`flex items-center space-x-2 ${availability["ch-double"] === false ? "opacity-50" : ""}`}>
<RadioGroupItem value="ch-double" id="ch-double" disabled={availability["ch-double"] === false} />
<Label htmlFor="ch-double" className={`font-normal text-sm ${availability["ch-double"] === false ? "cursor-not-allowed" : "cursor-pointer"}`}>
Bed in double room {availability["ch-double"] === false ? (
<span className="text-destructive font-medium">Sold Out</span>
) : (
<>356.30 (50.90/night)</>
)}
</Label>
</div>
</RadioGroup>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground mt-1">
I&apos;ll arrange my own accommodation
</p>
)}
</div>
</div>
</div>
{includeAccommodation && (
<p className="text-xs text-muted-foreground bg-muted/50 rounded-lg p-3">
We&apos;ll follow up closer to the event to confirm room assignments and accommodation details.
</p>
)}
{/* Food */}
<div className="py-4 border-b border-border">
<div className="flex items-start gap-3">
<Checkbox
id="include-food"
checked={wantFood}
onCheckedChange={(checked) => setWantFood(checked as boolean)}
className="mt-1"
/>
<div className="flex-1">
<Label htmlFor="include-food" className="font-medium cursor-pointer">
I would like to include food for the week
</Label>
<p className="text-sm text-muted-foreground 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.
</p>
</div>
</div>
</div>
{/* Processing fee */}
<div className="flex items-start justify-between py-3">
<div>
<div className="text-sm text-muted-foreground">Payment processing fee (2%)</div>
</div>
<span className="text-sm text-muted-foreground whitespace-nowrap ml-4">{processingFee.toFixed(2)}</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 Amount</div>
<div className="text-sm text-muted-foreground">
Ticket{includeAccommodation ? " + accommodation" : ""}
</div>
</div>
<span className="text-2xl font-bold text-primary">{totalPrice.toFixed(2)}</span>
</div>
</div>
</CardContent>
</Card>
{/* Payment Methods */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Payment Options</CardTitle>
<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="includeAccommodation" value={includeAccommodation ? "true" : "false"} />
<input type="hidden" name="accommodationType" value={accommodationType} />
{promoCode && <input type="hidden" name="promoCode" value={promoCode} />}
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
You'll be redirected to Mollie's secure checkout where you can pay by credit card,
SEPA bank transfer, iDEAL, PayPal, or other methods.
</p>
<div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={() => setStep("form")} className="flex-1">
Back to Form
</Button>
<Button type="submit" className="flex-1">
Proceed to Payment
</Button>
</div>
</div>
</form>
</CardContent>
</Card>
<div className="text-center text-sm text-muted-foreground">
<p>
All payments are processed securely through Mollie. You'll receive a confirmation email after successful
payment.
</p>
</div>
</div>
</main>
{/* Footer */}
<footer className="py-12 px-4 border-t border-border">
<div className="container mx-auto max-w-6xl">
<div className="grid sm:grid-cols-2 md:grid-cols-4 gap-8 mb-8">
<div>
<h3 className="font-bold mb-4">CCG 2026</h3>
<p className="text-sm text-muted-foreground">
Crypto Commons Gathering
<br />
August 16-23, 2026
</p>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Links</h3>
<ul className="space-y-2 text-sm">
<li>
<Link href="/register" className="text-muted-foreground hover:text-foreground transition-colors">
Register to Attend
</Link>
</li>
<li>
<Link href="/gallery" className="text-muted-foreground hover:text-foreground transition-colors">
Gallery
</Link>
</li>
<li>
<Link href="/about" className="text-muted-foreground hover:text-foreground transition-colors">
About CCG 2026
</Link>
</li>
<li>
<Link href="/directions" className="text-muted-foreground hover:text-foreground transition-colors">
Directions
</Link>
</li>
<li>
<Link
href="/transparency"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Financial Transparency
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Community</h3>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://t.me/+n5V_wDVKWrk1ZTBh"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Join the CCG26 Telegram
</Link>
</li>
<li>
<Link
href="https://t.me/+gZjhNaDswIc0ZDg0"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Join the Crypto Commons Association Telegram
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Partners</h3>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://www.commons-hub.at/"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Commons Hub
</Link>
</li>
<li>
<Link
href="https://crypto-commons.org"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Crypto Commons Association
</Link>
</li>
<li>
<Link href="/sponsorships" className="text-muted-foreground hover:text-foreground transition-colors">
Sponsorships
</Link>
</li>
</ul>
</div>
</div>
<div className="pt-8 border-t border-border text-center text-sm text-muted-foreground">
<p>This website is under Creative Commons license. Built with solidarity for the commons.</p>
</div>
</div>
</footer>
</div>
)
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b">
<div className="container mx-auto px-4 py-4">
<Link href="/" className="text-2xl font-bold text-primary">
CCG
</Link>
</div>
</header>
<main className="container mx-auto px-4 py-12 max-w-3xl">
<div className="text-center mb-12">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Register for CCG 2026</h1>
<p className="text-xl text-muted-foreground">August 16-23, 2026 at the Commons Hub in Austria</p>
</div>
{banner}
<Card>
<CardHeader>
<CardTitle>Registration Form</CardTitle>
<CardDescription>Tell us about yourself and what you'd like to bring to CCG</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="name">What's your name? *</Label>
<Input
id="name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
{/* Email */}
<div className="space-y-2">
<Label htmlFor="email">Email address *</Label>
<Input
id="email"
type="email"
required
placeholder="your@email.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
We&apos;ll send your registration confirmation and event updates here.
</p>
</div>
{/* Contact */}
<div className="space-y-2">
<Label htmlFor="contact">How can we contact you besides via email? *</Label>
<Input
id="contact"
required
placeholder="Telegram, Signal, phone, etc."
value={formData.contact}
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
/>
</div>
{/* Contributions */}
<div className="space-y-2">
<Label htmlFor="contributions">What inputs do you want to contribute to CCG? *</Label>
<Textarea
id="contributions"
required
placeholder="This could be a talk, workshop, research inquiry, prototype, game, performance, etc."
className="min-h-[120px]"
value={formData.contributions}
onChange={(e) => setFormData({ ...formData, contributions: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
This event is organized as an unconference, meaning that each participant is invited to co-create the
program.
</p>
</div>
{/* Expectations */}
<div className="space-y-2">
<Label htmlFor="expectations">What do you expect to gain from participating? *</Label>
<Textarea
id="expectations"
required
placeholder="What are you looking for in particular?"
className="min-h-[100px]"
value={formData.expectations}
onChange={(e) => setFormData({ ...formData, expectations: e.target.value })}
/>
</div>
{/* How heard */}
<div className="space-y-2">
<Label htmlFor="howHeard">How did you hear about CCG?</Label>
<Input
id="howHeard"
placeholder="First timers: Did anyone recommend it to you?"
value={formData.howHeard}
onChange={(e) => setFormData({ ...formData, howHeard: e.target.value })}
/>
</div>
{/* Dietary Requirements */}
<div className="space-y-3">
<Label>Dietary Requirements</Label>
<p className="text-sm text-muted-foreground">
Food will involve catering as well as self-prepared meals. Do you have any special dietary
requirements?
</p>
<div className="space-y-2">
{["vegetarian", "vegan", "gluten free", "lactose free"].map((diet) => (
<div key={diet} className="flex items-center space-x-2">
<Checkbox
id={diet}
checked={formData.dietary.includes(diet)}
onCheckedChange={(checked) => handleDietaryChange(diet, checked as boolean)}
/>
<Label htmlFor={diet} className="font-normal cursor-pointer">
{diet.charAt(0).toUpperCase() + diet.slice(1)}
</Label>
</div>
))}
<div className="flex items-start space-x-2 mt-2">
<Checkbox
id="other"
checked={!!formData.dietaryOther}
onCheckedChange={(checked) => {
if (!checked) setFormData({ ...formData, dietaryOther: "" })
}}
/>
<div className="flex-1">
<Label htmlFor="other" className="font-normal cursor-pointer">
Other:
</Label>
<Input
id="dietaryOther"
className="mt-1"
placeholder="Please specify..."
value={formData.dietaryOther}
onChange={(e) => setFormData({ ...formData, dietaryOther: e.target.value })}
/>
</div>
</div>
</div>
</div>
{/* Commons Crews Consent */}
<div className="space-y-3">
<Label>Commons Crews Participation *</Label>
<div className="bg-muted p-4 rounded-lg space-y-2 text-sm">
<p>
Part of the magic of CCG is that we consider the event a 'temporary commons' - we co-produce its
program as well as its leisure activities collectively.
</p>
<p>
This also involves joint care for the space and its maintenance needs, for emerging social and
emotional dynamics and for documentation of the sessions.
</p>
<p className="font-medium">
Besides active involvement in shaping the content of the event, you will be expected to contribute
to one or more 'commons crews' (kitchen, cleaning, documentation, facilitation, fire/water and
atmosphere).
</p>
</div>
<RadioGroup
required
value={formData.crewConsent}
onValueChange={(value) => setFormData({ ...formData, crewConsent: value })}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="cant-wait" id="cant-wait" />
<Label htmlFor="cant-wait" className="font-normal cursor-pointer">
Can't wait
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="yes-please" id="yes-please" />
<Label htmlFor="yes-please" className="font-normal cursor-pointer">
Yes please gimme work!
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="love-it" id="love-it" />
<Label htmlFor="love-it" className="font-normal cursor-pointer">
I love getting my hands dirty :D
</Label>
</div>
</RadioGroup>
</div>
<div className="pt-4">
<Button type="submit" className="w-full" size="lg" disabled={isSubmitting}>
{isSubmitting ? "Recording registration..." : "Continue to Payment"}
</Button>
</div>
<div className="text-sm text-muted-foreground text-center">
<p>
Questions? Contact us on{" "}
<a href="https://t.me/+n5V_wDVKWrk1ZTBh" className="text-primary hover:underline">
Telegram
</a>
</p>
</div>
</form>
</CardContent>
</Card>
</main>
{/* Footer */}
<footer className="py-12 px-4 border-t border-border">
<div className="container mx-auto max-w-6xl">
<div className="grid sm:grid-cols-2 md:grid-cols-4 gap-8 mb-8">
<div>
<h3 className="font-bold mb-4">CCG 2026</h3>
<p className="text-sm text-muted-foreground">
Crypto Commons Gathering
<br />
August 16-23, 2026
</p>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Links</h3>
<ul className="space-y-2 text-sm">
<li>
<Link href="/register" className="text-muted-foreground hover:text-foreground transition-colors">
Register to Attend
</Link>
</li>
<li>
<Link href="/gallery" className="text-muted-foreground hover:text-foreground transition-colors">
Gallery
</Link>
</li>
<li>
<Link href="/about" className="text-muted-foreground hover:text-foreground transition-colors">
About CCG 2026
</Link>
</li>
<li>
<Link href="/directions" className="text-muted-foreground hover:text-foreground transition-colors">
Directions
</Link>
</li>
<li>
<Link href="/transparency" className="text-muted-foreground hover:text-foreground transition-colors">
Financial Transparency
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Community</h3>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://t.me/+n5V_wDVKWrk1ZTBh"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Join the CCG26 Telegram
</Link>
</li>
<li>
<Link
href="https://t.me/+gZjhNaDswIc0ZDg0"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Join the Crypto Commons Association Telegram
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4 text-sm">Partners</h3>
<ul className="space-y-2 text-sm">
<li>
<Link
href="https://www.commons-hub.at/"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Commons Hub
</Link>
</li>
<li>
<Link
href="https://crypto-commons.org"
className="text-muted-foreground hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Crypto Commons Association
</Link>
</li>
<li>
<Link href="/sponsorships" className="text-muted-foreground hover:text-foreground transition-colors">
Sponsorships
</Link>
</li>
</ul>
</div>
</div>
<div className="pt-8 border-t border-border text-center text-sm text-muted-foreground">
<p>This website is under Creative Commons license. Built with solidarity for the commons.</p>
</div>
</div>
</footer>
</div>
)
}