feat:UX and fix drinkkaart payment logic

This commit is contained in:
2026-03-04 14:22:42 +01:00
parent 79869a9a21
commit f9d79c3879
26 changed files with 1432 additions and 283 deletions

View File

@@ -1,13 +1,24 @@
import { Link, useNavigate } from "@tanstack/react-router";
import { LogOut } from "lucide-react";
import { LogOut, User } from "lucide-react";
import { authClient } from "@/lib/auth-client";
interface HeaderProps {
userName?: string;
isAdmin?: boolean;
/** When true, the user is not authenticated — show login/signup CTAs */
isGuest?: boolean;
isVisible?: boolean;
/** When true, uses fixed positioning (for homepage scroll behavior) */
isHomepage?: boolean;
}
export function Header({ userName, isAdmin }: HeaderProps) {
export function Header({
userName,
isAdmin,
isGuest,
isVisible,
isHomepage,
}: HeaderProps) {
const navigate = useNavigate();
const handleSignOut = async () => {
@@ -15,48 +26,78 @@ export function Header({ userName, isAdmin }: HeaderProps) {
navigate({ to: "/" });
};
if (!userName) return null;
return (
<header className="border-white/10 border-b bg-[#214e51]/95 backdrop-blur-sm">
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-3">
<nav className="flex items-center gap-6">
<header
className={`border-white/10 border-b bg-[#214e51]/95 backdrop-blur-sm transition-transform duration-300 ${
isVisible ? "translate-y-0" : "-translate-y-full"
} ${isHomepage ? "fixed" : "sticky"} top-0 right-0 left-0 z-50`}
>
<div className="mx-auto flex max-w-8xl items-center justify-between px-3 py-2 sm:px-6 sm:py-3">
<nav className="flex items-center gap-3 sm:gap-6">
<Link
to="/"
className="font-['Intro',sans-serif] text-lg text-white/80 transition-opacity hover:opacity-100"
className="font-['Intro',sans-serif] text-base text-white/80 transition-opacity hover:opacity-100 sm:text-lg"
>
Kunstenkamp
</Link>
<Link
to="/drinkkaart"
search={{ topup: undefined }}
className="text-sm text-white/70 transition-colors hover:text-white"
activeProps={{ className: "text-white font-medium" }}
>
Drinkkaart
</Link>
{isAdmin && (
<Link
to="/admin"
className="text-sm text-white/70 transition-colors hover:text-white"
activeProps={{ className: "text-white font-medium" }}
>
Admin
</Link>
{!isGuest && (
<>
<Link
to="/account"
className="text-sm text-white/70 transition-colors hover:text-white"
activeProps={{ className: "text-white font-medium" }}
>
<span className="hidden sm:inline">Mijn Account</span>
<User className="h-4 w-4 sm:hidden" />
</Link>
{isAdmin && (
<Link
to="/admin"
className="text-sm text-white/70 transition-colors hover:text-white"
activeProps={{ className: "text-white font-medium" }}
>
Admin
</Link>
)}
</>
)}
</nav>
<div className="flex items-center gap-3">
<span className="text-sm text-white/60">{userName}</span>
<button
type="button"
onClick={handleSignOut}
className="flex items-center gap-1.5 rounded px-2 py-1 text-sm text-white/50 transition-colors hover:bg-white/10 hover:text-white"
>
<LogOut className="h-3.5 w-3.5" />
Uitloggen
</button>
</div>
{isGuest ? (
<div className="flex items-center gap-2 sm:gap-3">
<Link
to="/login"
className="text-sm text-white/70 transition-colors hover:text-white"
>
<span className="hidden sm:inline">Inloggen</span>
<span className="sm:hidden">Login</span>
</Link>
<span className="hidden text-white/20 sm:inline">·</span>
<Link
to="/login"
search={{ signup: "1" }}
className="rounded bg-white/10 px-2 py-1 text-white/80 text-xs transition-colors hover:bg-white/20 hover:text-white sm:px-3 sm:py-1.5 sm:text-sm"
>
<span className="hidden sm:inline">Account aanmaken</span>
<span className="sm:hidden">Registreer</span>
</Link>
</div>
) : (
<div className="flex items-center gap-2 sm:gap-3">
<span className="max-w-[80px] truncate text-white/60 text-xs sm:max-w-none sm:text-sm">
{userName}
</span>
<button
type="button"
onClick={handleSignOut}
className="flex items-center gap-1 rounded px-1.5 py-1 text-white/50 transition-colors hover:bg-white/10 hover:text-white sm:px-2"
title="Uitloggen"
>
<LogOut className="h-4 w-4" />
<span className="hidden text-xs sm:inline">Uitloggen</span>
</button>
</div>
)}
</div>
</header>
);

View File

@@ -1,18 +1,43 @@
"use client";
import { useRouterState } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { authClient } from "@/lib/auth-client";
import { Header } from "./Header";
/**
* Client-side header wrapper.
* Reads the current session and renders <Header> only when the user is logged in.
* Rendered in __root.tsx so it appears on every page.
*/
export function SiteHeader() {
const { data: session } = authClient.useSession();
const routerState = useRouterState();
const isHomepage = routerState.location.pathname === "/";
if (!session?.user) return null;
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!isHomepage) {
setIsVisible(true);
return;
}
const handleScroll = () => {
setIsVisible(window.scrollY > 50);
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [isHomepage]);
if (!session?.user) {
return <Header isGuest isVisible={isVisible} isHomepage={isHomepage} />;
}
const user = session.user as { name: string; role?: string };
return <Header userName={user.name} isAdmin={user.role === "admin"} />;
return (
<Header
userName={user.name}
isAdmin={user.role === "admin"}
isVisible={isVisible}
isHomepage={isHomepage}
/>
);
}

View File

@@ -1,23 +1,43 @@
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { PerformerForm } from "@/components/registration/PerformerForm";
import { SuccessScreen } from "@/components/registration/SuccessScreen";
import { TypeSelector } from "@/components/registration/TypeSelector";
import { WatcherForm } from "@/components/registration/WatcherForm";
import { authClient } from "@/lib/auth-client";
type RegistrationType = "performer" | "watcher";
interface SuccessState {
token: string;
email: string;
name: string;
}
export default function EventRegistrationForm() {
const { data: session } = authClient.useSession();
const [selectedType, setSelectedType] = useState<RegistrationType | null>(
null,
);
const [successToken, setSuccessToken] = useState<string | null>(null);
const [successState, setSuccessState] = useState<SuccessState | null>(null);
if (successToken) {
const isLoggedIn = !!session?.user;
const user = session?.user as { name?: string; email?: string } | undefined;
// Split "Jan De Smet" → firstName="Jan", lastName="De Smet"
const nameParts = (user?.name ?? "").trim().split(/\s+/);
const prefillFirstName = nameParts[0] ?? "";
const prefillLastName = nameParts.slice(1).join(" ");
const prefillEmail = user?.email ?? "";
if (successState) {
return (
<SuccessScreen
token={successToken}
token={successState.token}
email={successState.email}
name={successState.name}
onReset={() => {
setSuccessToken(null);
setSuccessState(null);
setSelectedType(null);
}}
/>
@@ -33,21 +53,62 @@ export default function EventRegistrationForm() {
<h2 className="mb-2 font-['Intro',sans-serif] text-3xl text-white md:text-4xl">
Schrijf je nu in!
</h2>
<p className="mb-12 max-w-3xl text-lg text-white/80 md:text-xl">
<p className="mb-8 max-w-3xl text-lg text-white/80 md:text-xl">
Doe je mee of kom je kijken? Kies je rol en vul het formulier in.
</p>
{/* Login nudge — shown only to guests */}
{!isLoggedIn && (
<div className="mb-10 flex flex-col gap-3 rounded-lg border border-[#52979b]/40 bg-[#52979b]/10 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-white/80">
<span className="mr-1 font-semibold text-teal-300">
Al een account?
</span>
Log in om je gegevens automatisch in te vullen en je Drinkkaart
direct te activeren na registratie.
</p>
<div className="flex shrink-0 items-center gap-3 text-sm">
<Link
to="/login"
className="font-medium text-teal-300 underline-offset-2 transition-colors hover:underline"
>
Inloggen
</Link>
<span className="text-white/30">·</span>
<Link
to="/login"
search={{ signup: "1" }}
className="text-white/60 transition-colors hover:text-white"
>
Nog geen account? Aanmaken
</Link>
</div>
</div>
)}
{!selectedType && <TypeSelector onSelect={setSelectedType} />}
{selectedType === "performer" && (
<PerformerForm
onBack={() => setSelectedType(null)}
onSuccess={setSuccessToken}
onSuccess={(token, email, name) =>
setSuccessState({ token, email, name })
}
prefillFirstName={prefillFirstName}
prefillLastName={prefillLastName}
prefillEmail={prefillEmail}
isLoggedIn={isLoggedIn}
/>
)}
{selectedType === "watcher" && (
<WatcherForm onBack={() => setSelectedType(null)} />
<WatcherForm
onBack={() => setSelectedType(null)}
prefillFirstName={prefillFirstName}
prefillLastName={prefillLastName}
prefillEmail={prefillEmail}
isLoggedIn={isLoggedIn}
/>
)}
</div>
</section>

View File

@@ -31,21 +31,21 @@ export default function Footer() {
Waar creativiteit tot leven komt
</p>
<div className="flex items-center justify-center gap-8 text-sm text-white/70">
<div className="flex flex-col items-center justify-center gap-2 text-sm text-white/70 md:flex-row md:gap-8">
<a
href="/privacy"
className="link-hover transition-colors hover:text-white"
>
Privacy Beleid
</a>
<span className="text-white/40">|</span>
<span className="hidden text-white/40 md:inline">|</span>
<a
href="/terms"
className="link-hover transition-colors hover:text-white"
>
Algemene Voorwaarden
</a>
<span className="text-white/40">|</span>
<span className="hidden text-white/40 md:inline">|</span>
<a
href="/contact"
className="link-hover transition-colors hover:text-white"
@@ -54,7 +54,7 @@ export default function Footer() {
</a>
{!isLoading && isAdmin && (
<>
<span className="text-white/40">|</span>
<span className="hidden text-white/40 md:inline">|</span>
<Link
to="/admin"
className="link-hover transition-colors hover:text-white"

View File

@@ -88,9 +88,15 @@ export default function Info() {
</div>
{/* Main Title */}
<h2 className="font-['Intro',sans-serif] font-black text-6xl text-white uppercase tracking-tight md:text-8xl lg:text-9xl">
<h2
className="font-['Intro',sans-serif] font-black text-white uppercase tracking-tight"
style={{ fontSize: "clamp(1rem, 8vw + 1rem, 8rem)" }}
>
Ongedesemd
<span className="block text-5xl md:text-7xl lg:text-8xl">
<span
className="block"
style={{ fontSize: "clamp(1rem, 6vw + 1rem, 6rem)" }}
>
Brood?!
</span>
</h2>
@@ -110,8 +116,16 @@ export default function Info() {
</div>
{/* Content Section */}
<div className="w-full px-12 py-16">
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col justify-center gap-16">
<div className="relative w-full px-12 py-16">
{/* Background texture */}
<div
className="pointer-events-none absolute inset-0 opacity-10"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}
/>
<div className="relative mx-auto flex w-full max-w-6xl flex-1 flex-col justify-center gap-16">
{/* Ongedesemd Brood Explanation - Full Width Special Treatment */}
<div className="relative flex flex-col gap-6 rounded-2xl border-2 border-white/20 bg-white/5 p-8 md:p-12">
<div className="absolute top-0 -left-4 h-full w-1 bg-gradient-to-b from-white/0 via-white/60 to-white/0" />

View File

@@ -21,14 +21,25 @@ interface PerformerErrors {
interface Props {
onBack: () => void;
onSuccess: (token: string) => void;
onSuccess: (token: string, email: string, name: string) => void;
prefillFirstName?: string;
prefillLastName?: string;
prefillEmail?: string;
isLoggedIn?: boolean;
}
export function PerformerForm({ onBack, onSuccess }: Props) {
export function PerformerForm({
onBack,
onSuccess,
prefillFirstName = "",
prefillLastName = "",
prefillEmail = "",
isLoggedIn = false,
}: Props) {
const [data, setData] = useState({
firstName: "",
lastName: "",
email: "",
firstName: prefillFirstName,
lastName: prefillLastName,
email: prefillEmail,
phone: "",
artForm: "",
experience: "",
@@ -42,7 +53,12 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
const submitMutation = useMutation({
...orpc.submitRegistration.mutationOptions(),
onSuccess: (result) => {
if (result.managementToken) onSuccess(result.managementToken);
if (result.managementToken)
onSuccess(
result.managementToken,
data.email.trim(),
`${data.firstName.trim()} ${data.lastName.trim()}`.trim(),
);
},
onError: (error) => {
toast.error(`Er is iets misgegaan: ${error.message}`);
@@ -249,14 +265,15 @@ export function PerformerForm({ onBack, onSuccess }: Props) {
id="p-email"
name="email"
value={data.email}
onChange={handleChange}
onBlur={handleBlur}
onChange={isLoggedIn ? undefined : handleChange}
onBlur={isLoggedIn ? undefined : handleBlur}
readOnly={isLoggedIn}
placeholder="jouw@email.be"
autoComplete="email"
inputMode="email"
aria-required="true"
aria-invalid={touched.email && !!errors.email}
className={inputCls(!!touched.email && !!errors.email)}
className={`${inputCls(!!touched.email && !!errors.email)}${isLoggedIn ? "cursor-not-allowed opacity-60" : ""}`}
/>
{touched.email && errors.email && (
<span className="text-red-300 text-sm" role="alert">

View File

@@ -2,10 +2,12 @@ import { useState } from "react";
interface Props {
token: string;
email?: string;
name?: string;
onReset: () => void;
}
export function SuccessScreen({ token, onReset }: Props) {
export function SuccessScreen({ token, email, name, onReset }: Props) {
const manageUrl =
typeof window !== "undefined"
? `${window.location.origin}/manage/${token}`
@@ -33,6 +35,11 @@ export function SuccessScreen({ token, onReset }: Props) {
setDrinkkaartPromptDismissed(true);
};
// Build signup URL with pre-filled email query param so /login can pre-fill
const signupUrl = email
? `/login?signup=1&email=${encodeURIComponent(email)}`
: "/login?signup=1";
return (
<section
id="registration"
@@ -90,27 +97,46 @@ export function SuccessScreen({ token, onReset }: Props) {
</button>
</div>
{/* Drinkkaart account prompt */}
{/* Account creation prompt */}
{!drinkkaartPromptDismissed && (
<div className="mt-8 rounded-lg border border-white/20 bg-white/10 p-6">
<h3 className="mb-2 font-['Intro',sans-serif] text-lg text-white">
Wil je een Drinkkaart?
<div className="mt-8 rounded-xl border border-teal-400/30 bg-teal-400/10 p-6">
<h3 className="mb-1 font-['Intro',sans-serif] text-lg text-white">
Maak een gratis account aan
</h3>
<p className="mb-4 text-sm text-white/70">
Maak een gratis account aan om je digitale Drinkkaart te
activeren en saldo op te laden vóór het evenement.
Met een account zie je je inschrijving, activeer je je
Drinkkaart en laad je saldo op vóór het evenement.
</p>
<div className="flex flex-wrap gap-3">
<ul className="mb-5 space-y-1.5 text-sm text-white/70">
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Beheer je inschrijving op één plek
</li>
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Digitale Drinkkaart met QR-code
</li>
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Saldo opladen vóór het evenement
</li>
</ul>
<div className="flex flex-wrap items-center gap-3">
<a
href="/login"
className="inline-flex items-center rounded-lg bg-white px-5 py-2.5 font-['Intro',sans-serif] text-[#214e51] text-sm transition-all hover:bg-gray-100"
href={signupUrl}
className="inline-flex items-center rounded-lg bg-white px-5 py-2.5 font-['Intro',sans-serif] text-[#214e51] text-sm transition-all hover:scale-105 hover:bg-gray-100"
>
Account aanmaken
{name && (
<span className="ml-1.5 font-normal text-xs opacity-60">
als {name}
</span>
)}
</a>
<button
type="button"
onClick={handleDismissPrompt}
className="text-sm text-white/50 underline underline-offset-2 transition-colors hover:text-white/80"
className="text-sm text-white/40 underline underline-offset-2 transition-colors hover:text-white/70"
>
Overslaan
</button>

View File

@@ -1,6 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import {
calculateDrinkCard,
type GuestEntry,
@@ -24,13 +25,231 @@ interface WatcherErrors {
interface Props {
onBack: () => void;
prefillFirstName?: string;
prefillLastName?: string;
prefillEmail?: string;
isLoggedIn?: boolean;
}
export function WatcherForm({ onBack }: Props) {
// ── Account creation modal shown after successful registration ─────────────
interface AccountModalProps {
prefillName: string;
prefillEmail: string;
onDone: () => void;
}
function AccountModal({
prefillName,
prefillEmail,
onDone,
}: AccountModalProps) {
const [name, setName] = useState(prefillName);
const [email] = useState(prefillEmail); // email is fixed (tied to registration)
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordError, setPasswordError] = useState<string | undefined>();
const signupMutation = useMutation({
mutationFn: async () => {
if (password.length < 8)
throw new Error("Wachtwoord moet minstens 8 tekens zijn");
if (password !== confirmPassword)
throw new Error("Wachtwoorden komen niet overeen");
const result = await authClient.signUp.email({ email, password, name });
if (result.error) throw new Error(result.error.message);
return result.data;
},
onSuccess: () => {
toast.success(
"Account aangemaakt! Je wordt nu doorgestuurd naar betaling.",
);
onDone();
},
onError: (error) => {
toast.error(`Account aanmaken mislukt: ${error.message}`);
},
});
const handlePasswordChange = (val: string) => {
setPassword(val);
if (passwordError) setPasswordError(undefined);
};
const handleConfirmChange = (val: string) => {
setConfirmPassword(val);
if (passwordError) setPasswordError(undefined);
};
const handleSignup = (e: React.FormEvent) => {
e.preventDefault();
if (password.length < 8) {
setPasswordError("Wachtwoord moet minstens 8 tekens zijn");
return;
}
if (password !== confirmPassword) {
setPasswordError("Wachtwoorden komen niet overeen");
return;
}
signupMutation.mutate();
};
// Receive the checkout URL from parent by exposing a setter via ref pattern
// (simpler: the parent renders the modal and passes a prop)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
<div className="w-full max-w-md rounded-2xl border border-white/10 bg-[#1a3d40] p-8 shadow-2xl">
{/* Header */}
<div className="mb-6 flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-teal-400/20 text-teal-300 text-xl">
</div>
<div>
<h2 className="font-['Intro',sans-serif] text-2xl text-white">
Inschrijving gelukt!
</h2>
<p className="mt-1 text-sm text-white/60">
Maak een gratis account aan om je Drinkkaart bij te houden.
</p>
</div>
</div>
{/* Value prop */}
<div className="mb-6 rounded-lg border border-teal-400/20 bg-teal-400/10 p-4">
<ul className="space-y-1.5 text-sm text-white/80">
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Zie je drinkkaart-saldo en QR-code
</li>
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Laad je kaart op vóór het evenement
</li>
<li className="flex items-center gap-2">
<span className="text-teal-300"></span>
Beheer je inschrijving op één plek
</li>
</ul>
</div>
{/* Signup form */}
<form onSubmit={handleSignup} className="space-y-4">
{/* Name */}
<div>
<label
className="mb-1.5 block text-sm text-white/60"
htmlFor="modal-name"
>
Naam
</label>
<input
id="modal-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="w-full rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder:text-white/30 focus:border-teal-400/60 focus:outline-none"
/>
</div>
{/* Email (read-only) */}
<div>
<label
className="mb-1.5 block text-sm text-white/60"
htmlFor="modal-email"
>
Email
</label>
<input
id="modal-email"
type="email"
value={email}
readOnly
className="w-full cursor-not-allowed rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-white/60"
/>
</div>
{/* Password */}
<div>
<label
className="mb-1.5 block text-sm text-white/60"
htmlFor="modal-password"
>
Wachtwoord
</label>
<input
id="modal-password"
type="password"
placeholder="Minstens 8 tekens"
value={password}
onChange={(e) => handlePasswordChange(e.target.value)}
required
minLength={8}
className="w-full rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder:text-white/30 focus:border-teal-400/60 focus:outline-none"
/>
</div>
{/* Confirm password */}
<div>
<label
className="mb-1.5 block text-sm text-white/60"
htmlFor="modal-confirm"
>
Bevestig wachtwoord
</label>
<input
id="modal-confirm"
type="password"
placeholder="Herhaal je wachtwoord"
value={confirmPassword}
onChange={(e) => handleConfirmChange(e.target.value)}
required
className="w-full rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder:text-white/30 focus:border-teal-400/60 focus:outline-none"
/>
</div>
{passwordError && (
<p className="text-red-300 text-sm">{passwordError}</p>
)}
<button
type="submit"
disabled={signupMutation.isPending}
className="w-full rounded-lg bg-white py-3 font-['Intro',sans-serif] text-[#214e51] transition-all hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
>
{signupMutation.isPending
? "Bezig..."
: "Account aanmaken & betalen"}
</button>
</form>
{/* Skip */}
<button
type="button"
onClick={onDone}
className="mt-4 w-full text-center text-sm text-white/40 transition-colors hover:text-white/70"
>
Overslaan, verder naar betaling
</button>
</div>
</div>
);
}
// ── Main watcher form ──────────────────────────────────────────────────────
export function WatcherForm({
onBack,
prefillFirstName = "",
prefillLastName = "",
prefillEmail = "",
isLoggedIn = false,
}: Props) {
const [data, setData] = useState({
firstName: "",
lastName: "",
email: "",
firstName: prefillFirstName,
lastName: prefillLastName,
email: prefillEmail,
phone: "",
extraQuestions: "",
});
@@ -40,16 +259,37 @@ export function WatcherForm({ onBack }: Props) {
const [guestErrors, setGuestErrors] = useState<GuestErrors[]>([]);
const [giftAmount, setGiftAmount] = useState(0);
// Modal state: shown after successful registration while we fetch checkout URL
const [modalState, setModalState] = useState<{
prefillName: string;
prefillEmail: string;
checkoutUrl: string;
} | null>(null);
const submitMutation = useMutation({
...orpc.submitRegistration.mutationOptions(),
onSuccess: async (result) => {
if (!result.managementToken) return;
// Redirect to Lemon Squeezy checkout immediately after registration
try {
const redirectUrl = isLoggedIn
? `${window.location.origin}/account?topup=success`
: undefined;
const checkout = await client.getCheckoutUrl({
token: result.managementToken,
redirectUrl,
});
window.location.href = checkout.checkoutUrl;
if (isLoggedIn) {
// Already logged in — skip the account-creation modal, go straight to checkout
window.location.href = checkout.checkoutUrl;
} else {
// Show the account-creation modal before redirecting to checkout
setModalState({
prefillName:
`${data.firstName.trim()} ${data.lastName.trim()}`.trim(),
prefillEmail: data.email.trim(),
checkoutUrl: checkout.checkoutUrl,
});
}
} catch (error) {
console.error("Checkout error:", error);
toast.error("Er is iets misgegaan bij het aanmaken van de betaling");
@@ -162,6 +402,19 @@ export function WatcherForm({ onBack }: Props) {
return (
<div>
{/* Account-creation interstitial modal */}
{modalState && (
<AccountModal
prefillName={modalState.prefillName}
prefillEmail={modalState.prefillEmail}
onDone={() => {
const url = modalState.checkoutUrl;
setModalState(null);
window.location.href = url;
}}
/>
)}
{/* Back + type header */}
<div className="mb-8 flex items-center gap-4">
<button
@@ -291,14 +544,15 @@ export function WatcherForm({ onBack }: Props) {
id="w-email"
name="email"
value={data.email}
onChange={handleChange}
onBlur={handleBlur}
onChange={isLoggedIn ? undefined : handleChange}
onBlur={isLoggedIn ? undefined : handleBlur}
readOnly={isLoggedIn}
placeholder="jouw@email.be"
autoComplete="email"
inputMode="email"
aria-required="true"
aria-invalid={touched.email && !!errors.email}
className={inputCls(!!touched.email && !!errors.email)}
className={`${inputCls(!!touched.email && !!errors.email)}${isLoggedIn ? "cursor-not-allowed opacity-60" : ""}`}
/>
{touched.email && errors.email && (
<span className="text-red-300 text-sm" role="alert">

View File

@@ -25,9 +25,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--normal-bg": "hsl(var(--popover))",
"--normal-text": "hsl(var(--popover-foreground))",
"--normal-border": "hsl(var(--border))",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}