diff --git a/apps/web/src/components/homepage/ArtForms.tsx b/apps/web/src/components/homepage/ArtForms.tsx index b51a053..1cf61f3 100644 --- a/apps/web/src/components/homepage/ArtForms.tsx +++ b/apps/web/src/components/homepage/ArtForms.tsx @@ -1,4 +1,4 @@ -import { Camera, Drama, Mic2, Music, Palette, PenTool } from "lucide-react"; +import { Drama, Music, Palette, PersonStanding } from "lucide-react"; const artForms = [ { @@ -6,15 +6,13 @@ const artForms = [ title: "Muziek", description: "Van akoestische singer-songwriter sets tot volledige band optredens. Ontdek je sound en deel je muziek met een warm publiek.", - trajectory: "Muziek Traject", color: "#d82560", }, { - icon: Drama, - title: "Theater", + icon: PersonStanding, + title: "Dans", description: - "Monologen, sketches, improvisatie of mime. Het podium is van jou — breng karakters tot leven en vertel verhalen die raken.", - trajectory: "Theater Traject", + "Contemporary, ballet, hiphop of freestyle. Beweging vertelt verhalen die woorden niet kunnen vangen.", color: "#52979b", }, { @@ -22,33 +20,15 @@ const artForms = [ title: "Beeldende Kunst", description: "Live schilderen, illustraties maken, of mixed media performances. Toon je creatieve proces terwijl het publiek toekijkt.", - trajectory: "Beeldende Kunst Traject", color: "#d09035", }, { - icon: PenTool, - title: "Woordkunst", + icon: Drama, + title: "Drama", description: - "Poëzie, spoken word, storytelling of rap. Laat je woorden dansen en raak het publiek met de kracht van taal.", - trajectory: "Woordkunst Traject", + "Monologen, sketches, improvisatie of mime. Het podium is van jou — breng karakters tot leven en vertel verhalen die raken.", color: "#214e51", }, - { - icon: Camera, - title: "Dans", - description: - "Contemporary, ballet, hiphop of freestyle. Beweging vertelt verhalen die woorden niet kunnen vangen.", - trajectory: "Dans Traject", - color: "#d82560", - }, - { - icon: Mic2, - title: "Comedy", - description: - "Stand-up, improv of cabaret. Maak het publiek aan het lachen met je unieke kijk op de wereld.", - trajectory: "Comedy Traject", - color: "#52979b", - }, ]; export default function ArtForms() { @@ -64,18 +44,18 @@ export default function ArtForms() { ervaringsdeskundigen.

-
+
{artForms.map((art, index) => { const IconComponent = art.icon; return (
{/* Color bar at top */}
); })} diff --git a/apps/web/src/components/homepage/EventRegistrationForm.tsx b/apps/web/src/components/homepage/EventRegistrationForm.tsx index 0b162fd..cf120ff 100644 --- a/apps/web/src/components/homepage/EventRegistrationForm.tsx +++ b/apps/web/src/components/homepage/EventRegistrationForm.tsx @@ -1,6 +1,7 @@ "use client"; import { useMutation } from "@tanstack/react-query"; + import { useCallback, useState } from "react"; import { toast } from "sonner"; import { orpc } from "@/utils/orpc"; @@ -27,6 +28,7 @@ export default function EventRegistrationForm() { }); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState>({}); + const [successToken, setSuccessToken] = useState(null); const validateField = useCallback( ( @@ -67,10 +69,10 @@ export default function EventRegistrationForm() { const submitMutation = useMutation({ ...orpc.submitRegistration.mutationOptions(), - onSuccess: () => { - toast.success( - "Registratie succesvol! We nemen binnenkort contact met je op.", - ); + onSuccess: (data) => { + if (data.managementToken) { + setSuccessToken(data.managementToken); + } setFormData({ firstName: "", lastName: "", @@ -89,6 +91,10 @@ export default function EventRegistrationForm() { }, }); + const handleReset = useCallback(() => { + setSuccessToken(null); + }, []); + const handleChange = useCallback( (e: React.ChangeEvent) => { const { name, value, type } = e.target; @@ -191,6 +197,76 @@ export default function EventRegistrationForm() { return `${baseClasses} ${errorClasses}`; }; + const manageUrl = successToken + ? `${typeof window !== "undefined" ? window.location.origin : ""}/manage/${successToken}` + : ""; + + if (successToken) { + return ( +
+
+
+
+ + + +
+

+ Gelukt! +

+

+ Je inschrijving is bevestigd. We sturen je zo dadelijk een + bevestigingsmail met alle details. +

+ +
+

+ Geen mail ontvangen? Controleer je spam-map of gebruik deze + link: +

+ + {manageUrl} + +
+ +
+ + Bekijk mijn inschrijving + + +
+
+
+
+ ); + } + return (
- Privacy Policy + Privacy Beleid | - Terms of Service + Algemene Voorwaarden | diff --git a/apps/web/src/components/homepage/Info.tsx b/apps/web/src/components/homepage/Info.tsx index 2a8189e..98b54f6 100644 --- a/apps/web/src/components/homepage/Info.tsx +++ b/apps/web/src/components/homepage/Info.tsx @@ -124,7 +124,7 @@ export default function Info() {

In de Bijbel staat ongedesemd brood (ook wel{" "} - matze genoemd) symbool voor + matzah genoemd) symbool voor eenvoud en zuiverheid, zonder de 'ballast' van desem.

diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 1f3a7ca..a0db2ae 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as LoginRouteImport } from './routes/login' import { Route as ContactRouteImport } from './routes/contact' import { Route as AdminRouteImport } from './routes/admin' import { Route as IndexRouteImport } from './routes/index' +import { Route as ManageTokenRouteImport } from './routes/manage.$token' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc/$' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' @@ -48,6 +49,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const ManageTokenRoute = ManageTokenRouteImport.update({ + id: '/manage/$token', + path: '/manage/$token', + getParentRoute: () => rootRouteImport, +} as any) const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ id: '/api/rpc/$', path: '/api/rpc/$', @@ -66,6 +72,7 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/manage/$token': typeof ManageTokenRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute } @@ -76,6 +83,7 @@ export interface FileRoutesByTo { '/login': typeof LoginRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/manage/$token': typeof ManageTokenRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute } @@ -87,6 +95,7 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/privacy': typeof PrivacyRoute '/terms': typeof TermsRoute + '/manage/$token': typeof ManageTokenRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute } @@ -99,6 +108,7 @@ export interface FileRouteTypes { | '/login' | '/privacy' | '/terms' + | '/manage/$token' | '/api/auth/$' | '/api/rpc/$' fileRoutesByTo: FileRoutesByTo @@ -109,6 +119,7 @@ export interface FileRouteTypes { | '/login' | '/privacy' | '/terms' + | '/manage/$token' | '/api/auth/$' | '/api/rpc/$' id: @@ -119,6 +130,7 @@ export interface FileRouteTypes { | '/login' | '/privacy' | '/terms' + | '/manage/$token' | '/api/auth/$' | '/api/rpc/$' fileRoutesById: FileRoutesById @@ -130,6 +142,7 @@ export interface RootRouteChildren { LoginRoute: typeof LoginRoute PrivacyRoute: typeof PrivacyRoute TermsRoute: typeof TermsRoute + ManageTokenRoute: typeof ManageTokenRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute } @@ -178,6 +191,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/manage/$token': { + id: '/manage/$token' + path: '/manage/$token' + fullPath: '/manage/$token' + preLoaderRoute: typeof ManageTokenRouteImport + parentRoute: typeof rootRouteImport + } '/api/rpc/$': { id: '/api/rpc/$' path: '/api/rpc/$' @@ -202,6 +222,7 @@ const rootRouteChildren: RootRouteChildren = { LoginRoute: LoginRoute, PrivacyRoute: PrivacyRoute, TermsRoute: TermsRoute, + ManageTokenRoute: ManageTokenRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute, } diff --git a/apps/web/src/routes/manage.$token.tsx b/apps/web/src/routes/manage.$token.tsx new file mode 100644 index 0000000..940eaaa --- /dev/null +++ b/apps/web/src/routes/manage.$token.tsx @@ -0,0 +1,430 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createFileRoute, Link, useParams } from "@tanstack/react-router"; +import { useState } from "react"; +import { toast } from "sonner"; +import { orpc } from "@/utils/orpc"; + +export const Route = createFileRoute("/manage/$token")({ + component: ManageRegistrationPage, +}); + +function ManageRegistrationPage() { + const { token } = useParams({ from: "/manage/$token" }); + const queryClient = useQueryClient(); + + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + phone: "", + wantsToPerform: false, + artForm: "", + experience: "", + extraQuestions: "", + }); + + const { data, isLoading, error } = useQuery({ + ...orpc.getRegistrationByToken.queryOptions({ input: { token } }), + }); + + const updateMutation = useMutation({ + ...orpc.updateRegistration.mutationOptions(), + onSuccess: () => { + toast.success("Inschrijving bijgewerkt!"); + queryClient.invalidateQueries({ queryKey: ["getRegistrationByToken"] }); + setIsEditing(false); + }, + onError: (err) => { + toast.error(`Opslaan mislukt: ${err.message}`); + }, + }); + + const cancelMutation = useMutation({ + ...orpc.cancelRegistration.mutationOptions(), + onSuccess: () => { + toast.success("Inschrijving geannuleerd"); + queryClient.invalidateQueries({ queryKey: ["getRegistrationByToken"] }); + }, + onError: (err) => { + toast.error(`Annuleren mislukt: ${err.message}`); + }, + }); + + const handleEdit = () => { + if (data) { + setFormData({ + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + phone: data.phone || "", + wantsToPerform: data.wantsToPerform ?? false, + artForm: data.artForm || "", + experience: data.experience || "", + extraQuestions: data.extraQuestions || "", + }); + setIsEditing(true); + } + }; + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + updateMutation.mutate({ + token, + firstName: formData.firstName.trim(), + lastName: formData.lastName.trim(), + email: formData.email.trim(), + phone: formData.phone.trim() || undefined, + wantsToPerform: formData.wantsToPerform, + artForm: formData.wantsToPerform + ? formData.artForm.trim() || undefined + : undefined, + experience: formData.wantsToPerform + ? formData.experience.trim() || undefined + : undefined, + extraQuestions: formData.extraQuestions.trim() || undefined, + }); + }; + + const handleCancel = () => { + if ( + confirm( + "Weet je zeker dat je je inschrijving wilt annuleren? Dit kan niet ongedaan worden gemaakt.", + ) + ) { + cancelMutation.mutate({ token }); + } + }; + + if (isLoading) { + return ( +
+
+
Laden...
+
+
+ ); + } + + if (error || !data || data.cancelledAt) { + return ( +
+
+ + ← Terug naar home + +

+ Inschrijving niet gevonden +

+

+ Deze link is ongeldig of de inschrijving is geannuleerd. +

+ + Nieuwe inschrijving + +
+
+ ); + } + + if (isEditing) { + return ( +
+
+ + ← Terug naar home + +

+ Bewerk inschrijving +

+ +
+
+
+ + + setFormData((p) => ({ ...p, firstName: e.target.value })) + } + className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none" + /> +
+
+ + + setFormData((p) => ({ ...p, lastName: e.target.value })) + } + className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none" + /> +
+
+ +
+ + + setFormData((p) => ({ ...p, email: e.target.value })) + } + className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none" + /> +
+ +
+ + + setFormData((p) => ({ ...p, phone: e.target.value })) + } + className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none" + /> +
+ +