diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..df49d7c --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/web/public/fonts/DMSans-latin-ext.woff2 b/apps/web/public/fonts/DMSans-latin-ext.woff2 new file mode 100644 index 0000000..db39c62 Binary files /dev/null and b/apps/web/public/fonts/DMSans-latin-ext.woff2 differ diff --git a/apps/web/public/fonts/DMSans-latin.woff2 b/apps/web/public/fonts/DMSans-latin.woff2 new file mode 100644 index 0000000..01383d7 Binary files /dev/null and b/apps/web/public/fonts/DMSans-latin.woff2 differ diff --git a/apps/web/src/components/CookieConsent.tsx b/apps/web/src/components/CookieConsent.tsx new file mode 100644 index 0000000..cd458bb --- /dev/null +++ b/apps/web/src/components/CookieConsent.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export function CookieConsent() { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const consent = localStorage.getItem("cookie-consent"); + if (!consent) { + setIsVisible(true); + } + }, []); + + const acceptCookies = () => { + localStorage.setItem("cookie-consent", "accepted"); + setIsVisible(false); + // Enable analytics tracking + if (window.plausible) { + window.plausible("pageview"); + } + }; + + const declineCookies = () => { + localStorage.setItem("cookie-consent", "declined"); + setIsVisible(false); + }; + + if (!isVisible) return null; + + return ( +
+
+
+

+ We gebruiken cookies +

+

+ We gebruiken analytische cookies om onze website te verbeteren. + + Meer informatie + +

+
+
+ + +
+
+
+ ); +} + +// Add to window type +declare global { + interface Window { + plausible: (event: string, options?: Record) => void; + } +} diff --git a/apps/web/src/components/homepage/ArtForms.tsx b/apps/web/src/components/homepage/ArtForms.tsx index c9c87b5..b51a053 100644 --- a/apps/web/src/components/homepage/ArtForms.tsx +++ b/apps/web/src/components/homepage/ArtForms.tsx @@ -53,59 +53,71 @@ const artForms = [ export default function ArtForms() { return ( -
+

Kies Je Traject

-

+

Kunstenkamp biedt trajecten aan voor verschillende kunstvormen. Ontdek waar jouw passie ligt en ontwikkel je talent onder begeleiding van ervaringsdeskundigen.

- {artForms.map((art) => { + {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 18e22d6..0b162fd 100644 --- a/apps/web/src/components/homepage/EventRegistrationForm.tsx +++ b/apps/web/src/components/homepage/EventRegistrationForm.tsx @@ -1,82 +1,223 @@ "use client"; import { useMutation } from "@tanstack/react-query"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { toast } from "sonner"; import { orpc } from "@/utils/orpc"; +interface FormErrors { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + artForm?: string; + experience?: string; +} + export default function EventRegistrationForm() { const [formData, setFormData] = useState({ firstName: "", lastName: "", email: "", phone: "", + wantsToPerform: false, artForm: "", experience: "", + extraQuestions: "", }); + const [errors, setErrors] = useState({}); + const [touched, setTouched] = useState>({}); + + const validateField = useCallback( + ( + name: string, + value: string, + wantsToPerform?: boolean, + ): string | undefined => { + switch (name) { + case "firstName": + if (!value.trim()) return "Voornaam is verplicht"; + if (value.length < 2) + return "Voornaam moet minimaal 2 tekens bevatten"; + break; + case "lastName": + if (!value.trim()) return "Achternaam is verplicht"; + if (value.length < 2) + return "Achternaam moet minimaal 2 tekens bevatten"; + break; + case "email": + if (!value.trim()) return "E-mail is verplicht"; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + return "Voer een geldig e-mailadres in"; + } + break; + case "phone": + if (value && !/^[\d\s\-+()]{10,}$/.test(value.replace(/\s/g, ""))) { + return "Voer een geldig telefoonnummer in"; + } + break; + case "artForm": + if (wantsToPerform && !value.trim()) return "Kunstvorm is verplicht"; + break; + } + return undefined; + }, + [], + ); const submitMutation = useMutation({ ...orpc.submitRegistration.mutationOptions(), onSuccess: () => { - toast.success("Registratie succesvol!"); + toast.success( + "Registratie succesvol! We nemen binnenkort contact met je op.", + ); setFormData({ firstName: "", lastName: "", email: "", phone: "", + wantsToPerform: false, artForm: "", experience: "", + extraQuestions: "", }); + setErrors({}); + setTouched({}); }, onError: (error) => { toast.error(`Er is iets misgegaan: ${error.message}`); }, }); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - submitMutation.mutate({ - firstName: formData.firstName, - lastName: formData.lastName, - email: formData.email, - phone: formData.phone || undefined, - artForm: formData.artForm, - experience: formData.experience || undefined, - }); - }; + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + const checked = (e.target as HTMLInputElement).checked; + const newValue = type === "checkbox" ? checked : value; - const handleChange = ( - e: React.ChangeEvent, - ) => { - setFormData((prev) => ({ - ...prev, - [e.target.name]: e.target.value, - })); + setFormData((prev) => { + const updated = { ...prev, [name]: newValue }; + // Clear performer fields when unchecking wantsToPerform + if (name === "wantsToPerform" && !checked) { + updated.artForm = ""; + updated.experience = ""; + } + return updated; + }); + + if (type !== "checkbox" && touched[name]) { + const error = validateField( + name, + value, + name === "artForm" ? formData.wantsToPerform : undefined, + ); + setErrors((prev) => ({ ...prev, [name]: error })); + } + }, + [touched, validateField, formData.wantsToPerform], + ); + + const handleBlur = useCallback( + (e: React.FocusEvent) => { + const { name, value } = e.target; + setTouched((prev) => ({ ...prev, [name]: true })); + const error = validateField( + name, + value, + name === "artForm" ? formData.wantsToPerform : undefined, + ); + setErrors((prev) => ({ ...prev, [name]: error })); + }, + [validateField, formData.wantsToPerform], + ); + + const validateForm = useCallback((): boolean => { + const newErrors: FormErrors = {}; + newErrors.firstName = validateField("firstName", formData.firstName); + newErrors.lastName = validateField("lastName", formData.lastName); + newErrors.email = validateField("email", formData.email); + newErrors.phone = validateField("phone", formData.phone); + newErrors.artForm = validateField( + "artForm", + formData.artForm, + formData.wantsToPerform, + ); + + setErrors(newErrors); + setTouched({ + firstName: true, + lastName: true, + email: true, + phone: true, + artForm: true, + experience: true, + }); + + return !Object.values(newErrors).some(Boolean); + }, [formData, validateField]); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!validateForm()) { + toast.error("Controleer je invoer"); + return; + } + submitMutation.mutate({ + 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, + }); + }, + [formData, submitMutation, validateForm], + ); + + const getInputClasses = (fieldName: keyof FormErrors) => { + const baseClasses = + "border-b bg-transparent pb-2 text-lg text-white placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-[#214e51] transition-all"; + const errorClasses = + touched[fieldName] && errors[fieldName] + ? "border-red-400" + : "border-white/30 focus:border-white"; + return `${baseClasses} ${errorClasses}`; }; return (
-
-

+
+

Schrijf je nu in!

-

- Doet dit jouw creatieve geest borellen? Vul nog even dit formulier in +

+ Doet dit jouw creatieve geest borrelen? Vul nog even dit formulier in

-
-
+ +
{/* Row 1: First Name + Last Name */} -
+
+ {touched.firstName && errors.firstName && ( + + {errors.firstName} + + )}
+ {touched.lastName && errors.lastName && ( + + {errors.lastName} + + )}
{/* Row 2: Email + Phone */} -
+
+ {touched.email && errors.email && ( + + {errors.email} + + )}
@@ -144,51 +329,158 @@ export default function EventRegistrationForm() { name="phone" value={formData.phone} onChange={handleChange} + onBlur={handleBlur} placeholder="06-12345678" - 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" + autoComplete="tel" + inputMode="tel" + aria-invalid={touched.phone && !!errors.phone} + aria-describedby={errors.phone ? "phone-error" : undefined} + className={getInputClasses("phone")} /> + {touched.phone && errors.phone && ( + + {errors.phone} + + )}
- {/* Row 3: Art Form (full width) */} -
- - -
- - {/* Row 4: Experience (full width) */} -
-
- + {/* Row 3: Wants to perform checkbox */} +