feat:admin

This commit is contained in:
2026-03-02 16:42:15 +01:00
parent 52563d80de
commit b343314931
19 changed files with 1221 additions and 8 deletions

View File

@@ -5,6 +5,7 @@
"scripts": {
"build": "vite build",
"serve": "vite preview",
"dev": "vite dev",
"dev:bare": "vite dev"
},
"dependencies": {

View File

@@ -1,6 +1,9 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { orpc } from "@/utils/orpc";
export default function EventRegistrationForm() {
const [formData, setFormData] = useState({
@@ -12,9 +15,34 @@ export default function EventRegistrationForm() {
experience: "",
});
const submitMutation = useMutation({
...orpc.submitRegistration.mutationOptions(),
onSuccess: () => {
toast.success("Registratie succesvol!");
setFormData({
firstName: "",
lastName: "",
email: "",
phone: "",
artForm: "",
experience: "",
});
},
onError: (error) => {
toast.error(`Er is iets misgegaan: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Form submitted:", formData);
submitMutation.mutate({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phone: formData.phone || undefined,
artForm: formData.artForm,
experience: formData.experience || undefined,
});
};
const handleChange = (
@@ -57,6 +85,7 @@ export default function EventRegistrationForm() {
value={formData.firstName}
onChange={handleChange}
placeholder="Jouw voornaam"
required
className="border-white/30 border-b bg-transparent pb-2 font-['Intro',sans-serif] text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
/>
</div>
@@ -75,6 +104,7 @@ export default function EventRegistrationForm() {
value={formData.lastName}
onChange={handleChange}
placeholder="Jouw achternaam"
required
className="border-white/30 border-b bg-transparent pb-2 font-['Intro',sans-serif] text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
/>
</div>
@@ -96,6 +126,7 @@ export default function EventRegistrationForm() {
value={formData.email}
onChange={handleChange}
placeholder="jouw@email.nl"
required
className="border-white/30 border-b bg-transparent pb-2 font-['Intro',sans-serif] text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
/>
</div>
@@ -134,6 +165,7 @@ export default function EventRegistrationForm() {
value={formData.artForm}
onChange={handleChange}
placeholder="Muziek, Theater, Dans, etc."
required
className="border-white/30 border-b bg-transparent pb-2 font-['Intro',sans-serif] text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
/>
</div>
@@ -164,9 +196,10 @@ export default function EventRegistrationForm() {
<div className="mt-auto flex flex-col items-center gap-4 pt-12">
<button
type="submit"
className="bg-white px-12 py-4 font-['Intro',sans-serif] text-2xl text-[#214e51] transition-all hover:scale-105 hover:bg-gray-100"
disabled={submitMutation.isPending}
className="bg-white px-12 py-4 font-['Intro',sans-serif] text-2xl text-[#214e51] transition-all hover:scale-105 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
>
Bevestigen
{submitMutation.isPending ? "Bezig..." : "Bevestigen"}
</button>
<p className="text-center font-['Intro',sans-serif] text-sm text-white/60">

View File

@@ -1,4 +1,21 @@
"use client";
import { Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { authClient } from "@/lib/auth-client";
export default function Footer() {
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
authClient.getSession().then((session) => {
const user = session.data?.user as { role?: string } | undefined;
setIsAdmin(user?.role === "admin");
setIsLoading(false);
});
}, []);
return (
<footer className="snap-section relative z-40 flex h-[250px] flex-col items-center justify-center bg-[#d09035]">
<div className="text-center">
@@ -21,6 +38,14 @@ export default function Footer() {
<a href="/contact" className="transition-colors hover:text-white">
Contact
</a>
{!isLoading && isAdmin && (
<>
<span className="text-white/40">|</span>
<Link to="/admin" className="transition-colors hover:text-white">
Admin
</Link>
</>
)}
</div>
<div className="mt-6 font-['Intro',sans-serif] text-white/50 text-xs">

View File

@@ -9,10 +9,22 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AdminRouteImport } from './routes/admin'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc/$'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AdminRoute = AdminRouteImport.update({
id: '/admin',
path: '/admin',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -31,36 +43,58 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/admin': typeof AdminRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/api/auth/$' | '/api/rpc/$'
fullPaths: '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/api/auth/$' | '/api/rpc/$'
id: '__root__' | '/' | '/api/auth/$' | '/api/rpc/$'
to: '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
id: '__root__' | '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRoute: typeof AdminRoute
LoginRoute: typeof LoginRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/admin': {
id: '/admin'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -87,6 +121,8 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRoute: AdminRoute,
LoginRoute: LoginRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute,
}

View File

@@ -0,0 +1,463 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import {
createFileRoute,
Link,
redirect,
useNavigate,
} from "@tanstack/react-router";
import { Check, Download, LogOut, Search, Users, X } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { orpc } from "@/utils/orpc";
export const Route = createFileRoute("/admin")({
component: AdminPage,
beforeLoad: async () => {
const session = await authClient.getSession();
if (!session.data?.user) {
throw redirect({ to: "/login" });
}
const user = session.data.user as { role?: string };
if (user.role !== "admin") {
throw redirect({ to: "/login" });
}
},
});
function AdminPage() {
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [artForm, setArtForm] = useState("");
const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState("");
const [page, setPage] = useState(1);
const pageSize = 20;
const statsQuery = useQuery(orpc.getRegistrationStats.queryOptions());
const registrationsQuery = useQuery(
orpc.getRegistrations.queryOptions({
input: {
search: search || undefined,
artForm: artForm || undefined,
fromDate: fromDate || undefined,
toDate: toDate || undefined,
page,
pageSize,
},
}),
);
const adminRequestsQuery = useQuery(orpc.getAdminRequests.queryOptions());
const exportMutation = useMutation({
...orpc.exportRegistrations.mutationOptions(),
onSuccess: (data: { csv: string; filename: string }) => {
const blob = new Blob([data.csv], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = data.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
});
const approveRequestMutation = useMutation({
...orpc.approveAdminRequest.mutationOptions(),
onSuccess: () => {
toast.success("Admin toegang goedgekeurd");
adminRequestsQuery.refetch();
},
onError: (error) => {
toast.error(`Fout: ${error.message}`);
},
});
const rejectRequestMutation = useMutation({
...orpc.rejectAdminRequest.mutationOptions(),
onSuccess: () => {
toast.success("Admin toegang geweigerd");
adminRequestsQuery.refetch();
},
onError: (error) => {
toast.error(`Fout: ${error.message}`);
},
});
const handleSignOut = async () => {
await authClient.signOut();
navigate({ to: "/" });
};
const handleExport = () => {
exportMutation.mutate(undefined);
};
const handleApprove = (requestId: string) => {
approveRequestMutation.mutate({ requestId });
};
const handleReject = (requestId: string) => {
rejectRequestMutation.mutate({ requestId });
};
const stats = statsQuery.data;
const registrations = registrationsQuery.data?.data ?? [];
const pagination = registrationsQuery.data?.pagination;
const adminRequests = adminRequestsQuery.data ?? [];
const pendingRequests = adminRequests.filter((r) => r.status === "pending");
return (
<div className="min-h-screen bg-[#214e51]">
{/* Header */}
<header className="border-white/10 border-b bg-[#214e51]/95 px-8 py-6">
<div className="mx-auto flex max-w-7xl items-center justify-between">
<div className="flex items-center gap-4">
<Link to="/" className="text-white hover:opacity-80">
Terug naar website
</Link>
<h1 className="font-['Intro',sans-serif] text-3xl text-white">
Admin Dashboard
</h1>
</div>
<Button
onClick={handleSignOut}
variant="outline"
className="border-white/30 bg-transparent text-white hover:bg-white/10"
>
<LogOut className="mr-2 h-4 w-4" />
Uitloggen
</Button>
</div>
</header>
{/* Main Content */}
<main className="mx-auto max-w-7xl p-8">
{/* Pending Admin Requests */}
{pendingRequests.length > 0 && (
<Card className="mb-6 border-yellow-500/30 bg-yellow-500/10">
<CardHeader>
<CardTitle className="font-['Intro',sans-serif] text-xl text-yellow-200">
Openstaande Admin Aanvragen ({pendingRequests.length})
</CardTitle>
<CardDescription className="text-yellow-200/60">
Gebruikers die admin toegang hebben aangevraagd
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{pendingRequests.map((request) => (
<div
key={request.id}
className="flex items-center justify-between rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-4"
>
<div>
<p className="font-medium text-white">
{request.userName}
</p>
<p className="text-sm text-white/60">
{request.userEmail}
</p>
<p className="text-white/40 text-xs">
Aangevraagd:{" "}
{new Date(request.requestedAt).toLocaleDateString(
"nl-BE",
)}
</p>
</div>
<div className="flex gap-2">
<Button
onClick={() => handleApprove(request.id)}
disabled={approveRequestMutation.isPending}
size="sm"
className="bg-green-600 text-white hover:bg-green-700"
>
<Check className="mr-1 h-4 w-4" />
Goedkeuren
</Button>
<Button
onClick={() => handleReject(request.id)}
disabled={rejectRequestMutation.isPending}
size="sm"
variant="outline"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
>
<X className="mr-1 h-4 w-4" />
Weigeren
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Stats Cards */}
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-3">
<Card className="border-white/10 bg-white/5">
<CardHeader className="pb-2">
<CardDescription className="text-white/60">
Totaal inschrijvingen
</CardDescription>
<CardTitle className="font-['Intro',sans-serif] text-4xl text-white">
{stats?.total ?? 0}
</CardTitle>
</CardHeader>
<CardContent>
<Users className="h-5 w-5 text-white/40" />
</CardContent>
</Card>
<Card className="border-white/10 bg-white/5">
<CardHeader className="pb-2">
<CardDescription className="text-white/60">
Vandaag ingeschreven
</CardDescription>
<CardTitle className="font-['Intro',sans-serif] text-4xl text-white">
{stats?.today ?? 0}
</CardTitle>
</CardHeader>
<CardContent>
<span className="text-sm text-white/40">
Nieuwe registraties vandaag
</span>
</CardContent>
</Card>
<Card className="border-white/10 bg-white/5">
<CardHeader className="pb-2">
<CardDescription className="text-white/60">
Per kunstvorm
</CardDescription>
<div className="mt-2 space-y-1">
{stats?.byArtForm.slice(0, 5).map((item) => (
<div
key={item.artForm}
className="flex items-center justify-between text-sm"
>
<span className="text-white/80">{item.artForm}</span>
<span className="font-['Intro',sans-serif] text-white">
{item.count}
</span>
</div>
))}
</div>
</CardHeader>
</Card>
</div>
{/* Filters */}
<Card className="mb-6 border-white/10 bg-white/5">
<CardHeader>
<CardTitle className="font-['Intro',sans-serif] text-white text-xl">
Filters
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<label
htmlFor="search"
className="mb-2 block text-sm text-white/60"
>
Zoeken
</label>
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-white/40" />
<Input
id="search"
placeholder="Naam of email..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="border-white/20 bg-white/10 pl-10 text-white placeholder:text-white/40"
/>
</div>
</div>
<div>
<label
htmlFor="artForm"
className="mb-2 block text-sm text-white/60"
>
Kunstvorm
</label>
<Input
id="artForm"
placeholder="Filter op kunstvorm..."
value={artForm}
onChange={(e) => setArtForm(e.target.value)}
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
/>
</div>
<div>
<label
htmlFor="fromDate"
className="mb-2 block text-sm text-white/60"
>
Vanaf
</label>
<Input
id="fromDate"
type="date"
value={fromDate}
onChange={(e) => setFromDate(e.target.value)}
className="border-white/20 bg-white/10 text-white [color-scheme:dark]"
/>
</div>
<div>
<label
htmlFor="toDate"
className="mb-2 block text-sm text-white/60"
>
Tot
</label>
<Input
id="toDate"
type="date"
value={toDate}
onChange={(e) => setToDate(e.target.value)}
className="border-white/20 bg-white/10 text-white [color-scheme:dark]"
/>
</div>
</div>
</CardContent>
</Card>
{/* Export Button */}
<div className="mb-6 flex items-center justify-between">
<p className="text-white/60">
{pagination?.total ?? 0} registraties gevonden
</p>
<Button
onClick={handleExport}
disabled={exportMutation.isPending}
className="bg-white text-[#214e51] hover:bg-white/90"
>
<Download className="mr-2 h-4 w-4" />
{exportMutation.isPending ? "Exporteren..." : "Exporteer CSV"}
</Button>
</div>
{/* Registrations Table */}
<Card className="border-white/10 bg-white/5">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-white/10 border-b">
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Naam
</th>
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Email
</th>
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Telefoon
</th>
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Kunstvorm
</th>
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Ervaring
</th>
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
Datum
</th>
</tr>
</thead>
<tbody>
{registrationsQuery.isLoading ? (
<tr>
<td
colSpan={6}
className="px-6 py-8 text-center text-white/60"
>
Laden...
</td>
</tr>
) : registrations.length === 0 ? (
<tr>
<td
colSpan={6}
className="px-6 py-8 text-center text-white/60"
>
Geen registraties gevonden
</td>
</tr>
) : (
registrations.map((reg) => (
<tr
key={reg.id}
className="border-white/5 border-b hover:bg-white/5"
>
<td className="px-6 py-4 text-white">
{reg.firstName} {reg.lastName}
</td>
<td className="px-6 py-4 text-white/80">{reg.email}</td>
<td className="px-6 py-4 text-white/80">
{reg.phone || "-"}
</td>
<td className="px-6 py-4 text-white/80">
{reg.artForm}
</td>
<td className="px-6 py-4 text-white/80">
{reg.experience || "-"}
</td>
<td className="px-6 py-4 text-white/60">
{new Date(reg.createdAt).toLocaleDateString("nl-BE")}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Pagination */}
{pagination && pagination.totalPages > 1 && (
<div className="mt-6 flex items-center justify-center gap-2">
<Button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
variant="outline"
className="border-white/20 bg-transparent text-white hover:bg-white/10 disabled:opacity-50"
>
Vorige
</Button>
<span className="mx-4 text-white">
Pagina {page} van {pagination.totalPages}
</span>
<Button
onClick={() =>
setPage((p) => Math.min(pagination.totalPages, p + 1))
}
disabled={page === pagination.totalPages}
variant="outline"
className="border-white/20 bg-transparent text-white hover:bg-white/10 disabled:opacity-50"
>
Volgende
</Button>
</div>
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,272 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { orpc } from "@/utils/orpc";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
const navigate = useNavigate();
const [isSignup, setIsSignup] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const sessionQuery = useQuery({
queryKey: ["session"],
queryFn: () => authClient.getSession(),
});
const loginMutation = useMutation({
mutationFn: async ({
email,
password,
}: {
email: string;
password: string;
}) => {
const result = await authClient.signIn.email({
email,
password,
});
if (result.error) {
throw new Error(result.error.message);
}
return result.data;
},
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: "/" });
}
});
},
onError: (error) => {
toast.error(`Login mislukt: ${error.message}`);
},
});
const signupMutation = useMutation({
mutationFn: async ({
email,
password,
name,
}: {
email: string;
password: string;
name: string;
}) => {
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! Wacht op goedkeuring van een admin.");
setIsSignup(false);
setName("");
setEmail("");
setPassword("");
},
onError: (error) => {
toast.error(`Registratie mislukt: ${error.message}`);
},
});
const requestAdminMutation = useMutation({
...orpc.requestAdminAccess.mutationOptions(),
onSuccess: () => {
toast.success("Admin toegang aangevraagd!");
},
onError: (error) => {
toast.error(`Aanvraag mislukt: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isSignup) {
signupMutation.mutate({ email, password, name });
} else {
loginMutation.mutate({ email, password });
}
};
const handleRequestAdmin = () => {
requestAdminMutation.mutate();
};
// If already logged in as admin, redirect
if (sessionQuery.data?.data?.user) {
const user = sessionQuery.data.data.user as { role?: string };
if (user.role === "admin") {
navigate({ to: "/admin" });
return null;
}
}
const isLoggedIn = !!sessionQuery.data?.data?.user;
const user = sessionQuery.data?.data?.user as
| { role?: string; name?: string }
| undefined;
return (
<div className="flex min-h-screen items-center justify-center bg-[#214e51] px-4">
<Card className="w-full max-w-md border-white/10 bg-white/5">
<CardHeader className="text-center">
<CardTitle className="font-['Intro',sans-serif] text-3xl text-white">
{isLoggedIn
? "Admin Toegang"
: isSignup
? "Account Aanmaken"
: "Inloggen"}
</CardTitle>
<CardDescription className="text-white/60">
{isLoggedIn
? `Welkom, ${user?.name}`
: isSignup
? "Maak een account aan om toegang te krijgen"
: "Log in om toegang te krijgen tot het admin dashboard"}
</CardDescription>
</CardHeader>
<CardContent>
{isLoggedIn ? (
<div className="space-y-4">
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4">
<p className="text-center text-yellow-200">
Je bent ingelogd maar hebt geen admin toegang.
</p>
</div>
<Button
onClick={handleRequestAdmin}
disabled={requestAdminMutation.isPending}
className="w-full bg-white text-[#214e51] hover:bg-white/90"
>
{requestAdminMutation.isPending
? "Bezig..."
: "Vraag Admin Toegang Aan"}
</Button>
<Button
onClick={() => authClient.signOut()}
variant="outline"
className="w-full border-white/20 bg-transparent text-white hover:bg-white/10"
>
Uitloggen
</Button>
<Link
to="/"
className="block text-center text-sm text-white/60 hover:text-white"
>
Terug naar website
</Link>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{isSignup && (
<div>
<label
htmlFor="name"
className="mb-2 block text-sm text-white/60"
>
Naam
</label>
<Input
id="name"
type="text"
placeholder="Jouw naam"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
/>
</div>
)}
<div>
<label
htmlFor="email"
className="mb-2 block text-sm text-white/60"
>
Email
</label>
<Input
id="email"
type="email"
placeholder="jouw@email.be"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
/>
</div>
<div>
<label
htmlFor="password"
className="mb-2 block text-sm text-white/60"
>
Wachtwoord
</label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="border-white/20 bg-white/10 text-white placeholder:text-white/40"
/>
</div>
<Button
type="submit"
disabled={loginMutation.isPending || signupMutation.isPending}
className="w-full bg-white text-[#214e51] hover:bg-white/90"
>
{loginMutation.isPending || signupMutation.isPending
? "Bezig..."
: isSignup
? "Account Aanmaken"
: "Inloggen"}
</Button>
<div className="flex flex-col gap-2 text-center">
<button
type="button"
onClick={() => setIsSignup(!isSignup)}
className="text-sm text-white/60 hover:text-white"
>
{isSignup
? "Al een account? Log in"
: "Nog geen account? Registreer"}
</button>
<Link to="/" className="text-sm text-white/60 hover:text-white">
Terug naar website
</Link>
</div>
</form>
)}
</CardContent>
</Card>
</div>
);
}