diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx
index 10a6ea1..74ebed8 100644
--- a/apps/web/src/components/Header.tsx
+++ b/apps/web/src/components/Header.tsx
@@ -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 (
-
-
-
diff --git a/apps/web/src/components/homepage/Footer.tsx b/apps/web/src/components/homepage/Footer.tsx
index 220278f..9411adb 100644
--- a/apps/web/src/components/homepage/Footer.tsx
+++ b/apps/web/src/components/homepage/Footer.tsx
@@ -31,21 +31,21 @@ export default function Footer() {
Waar creativiteit tot leven komt
- 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.
-
+
+
+ ✓
+ Beheer je inschrijving op één plek
+
+
+ ✓
+ Digitale Drinkkaart met QR-code
+
+
+ ✓
+ Saldo opladen vóór het evenement
+
+
+
Account aanmaken
+ {name && (
+
+ als {name}
+
+ )}
diff --git a/apps/web/src/components/registration/WatcherForm.tsx b/apps/web/src/components/registration/WatcherForm.tsx
index 9487895..60e9639 100644
--- a/apps/web/src/components/registration/WatcherForm.tsx
+++ b/apps/web/src/components/registration/WatcherForm.tsx
@@ -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();
+
+ 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 (
+
+
+ {/* Header */}
+
+
+ ✓
+
+
+
+ Inschrijving gelukt!
+
+
+ Maak een gratis account aan om je Drinkkaart bij te houden.
+
+
+
+
+ {/* Value prop */}
+
+
+
+ ✓
+ Zie je drinkkaart-saldo en QR-code
+
+
+ ✓
+ Laad je kaart op vóór het evenement
+
+
+ ✓
+ Beheer je inschrijving op één plek
+
+
+
+
+ {/* Signup form */}
+
+
+ {/* Skip */}
+
+
+
+ );
+}
+
+// ── 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([]);
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 (
- );
-}
diff --git a/apps/web/src/routes/login.tsx b/apps/web/src/routes/login.tsx
index 7564077..7b0a2a4 100644
--- a/apps/web/src/routes/login.tsx
+++ b/apps/web/src/routes/login.tsx
@@ -15,13 +15,24 @@ import { authClient } from "@/lib/auth-client";
import { orpc } from "@/utils/orpc";
export const Route = createFileRoute("/login")({
+ validateSearch: (search: Record) => {
+ const signup =
+ search.signup === "1" || search.signup === "true" ? "1" : undefined;
+ const email = typeof search.email === "string" ? search.email : undefined;
+ // Only return defined keys so redirects without search still typecheck
+ return {
+ ...(signup !== undefined && { signup }),
+ ...(email !== undefined && { email }),
+ } as { signup?: "1"; email?: string };
+ },
component: LoginPage,
});
function LoginPage() {
const navigate = useNavigate();
- const [isSignup, setIsSignup] = useState(false);
- const [email, setEmail] = useState("");
+ const search = Route.useSearch();
+ const [isSignup, setIsSignup] = useState(() => search.signup === "1");
+ const [email, setEmail] = useState(() => search.email ?? "");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
@@ -49,13 +60,12 @@ function LoginPage() {
},
onSuccess: () => {
toast.success("Succesvol ingelogd!");
- // Check role and redirect
authClient.getSession().then((session) => {
const user = session.data?.user as { role?: string } | undefined;
if (user?.role === "admin") {
navigate({ to: "/admin" });
} else {
- navigate({ to: "/" });
+ navigate({ to: "/account" });
}
});
},
@@ -85,11 +95,7 @@ function LoginPage() {
return result.data;
},
onSuccess: () => {
- toast.success("Account aangemaakt! Wacht op goedkeuring van een admin.");
- setIsSignup(false);
- setName("");
- setEmail("");
- setPassword("");
+ navigate({ to: "/account", search: { welkom: "1" } });
},
onError: (error) => {
toast.error(`Registratie mislukt: ${error.message}`);
@@ -119,13 +125,15 @@ function LoginPage() {
requestAdminMutation.mutate(undefined);
};
- // If already logged in as admin, redirect
+ // If already logged in, redirect to appropriate page
if (sessionQuery.data?.data?.user) {
const user = sessionQuery.data.data.user as { role?: string };
if (user.role === "admin") {
navigate({ to: "/admin" });
- return null;
+ } else {
+ navigate({ to: "/account" });
}
+ return null;
}
const isLoggedIn = !!sessionQuery.data?.data?.user;
@@ -134,51 +142,64 @@ function LoginPage() {
| undefined;
return (
-
-
+
+
{isLoggedIn
- ? "Admin Toegang"
+ ? `Welkom, ${user?.name}`
: isSignup
? "Account Aanmaken"
: "Inloggen"}
{isLoggedIn
- ? `Welkom, ${user?.name}`
+ ? "Je bent al ingelogd"
: isSignup
- ? "Maak een account aan om toegang te krijgen"
- : "Log in om toegang te krijgen tot het admin dashboard"}
+ ? "Maak een gratis account aan voor je Drinkkaart en inschrijving"
+ : "Log in om je account te bekijken"}
{isLoggedIn ? (
-
-
- Je bent ingelogd maar hebt geen admin toegang.
-
-
+ {/* Primary CTA: go to account */}
+
+ {/* Secondary: request admin access */}
+
+
+ Admin-toegang aanvragen?
+
+
+
+
← Terug naar website
diff --git a/apps/web/src/routes/manage.$token.tsx b/apps/web/src/routes/manage.$token.tsx
index 015919a..4c98809 100644
--- a/apps/web/src/routes/manage.$token.tsx
+++ b/apps/web/src/routes/manage.$token.tsx
@@ -22,8 +22,8 @@ export const Route = createFileRoute("/manage/$token")({
function PageShell({ children }: { children: React.ReactNode }) {
return (
-
-
{children}
+
+
{children}
);
}
@@ -31,10 +31,10 @@ function PageShell({ children }: { children: React.ReactNode }) {
function BackLink() {
return (
- ← Terug naar home
+ ← Terug naar account
);
}
@@ -427,7 +427,7 @@ function EditForm({ token, initialData, onCancel, onSaved }: EditFormProps) {
onChange={(e) =>
setFormData((p) => ({ ...p, extraQuestions: e.target.value }))
}
- className="w-full resize-none bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:outline-none"
+ className="w-full border border-white/10 resize-none bg-transparent p-2 text-lg text-white placeholder:text-white/40 focus:outline-none"
/>