feat:accessibility, new routes, cookie consent, and UI improvements
- Add contact, privacy, and terms pages - Add CookieConsent component with accept/decline and localStorage - Add self-hosted DM Sans font with @font-face definitions - Improve registration form with field validation, blur handlers, and performer toggle - Redesign Info section with 'Ongedesemd Brood' hero and FAQ layout - Remove scroll-snap behavior from all sections - Add reduced motion support and selection color theming - Add SVG favicon and SEO meta tags in root layout - Improve accessibility: aria attributes, semantic HTML, focus styles - Add link-hover underline animation utility
This commit is contained in:
75
apps/web/src/components/CookieConsent.tsx
Normal file
75
apps/web/src/components/CookieConsent.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="Cookie toestemming"
|
||||
className="fixed right-0 bottom-0 left-0 z-50 border-white/10 border-t bg-[#214e51] p-4 shadow-lg md:p-6"
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-['Intro',sans-serif] text-white">
|
||||
<strong>We gebruiken cookies</strong>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/80">
|
||||
We gebruiken analytische cookies om onze website te verbeteren.
|
||||
<a href="/privacy" className="underline hover:text-white">
|
||||
Meer informatie
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={declineCookies}
|
||||
className="px-4 py-2 text-sm text-white/80 transition-colors hover:text-white"
|
||||
>
|
||||
Weigeren
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acceptCookies}
|
||||
className="bg-white px-6 py-2 font-['Intro',sans-serif] text-[#214e51] text-sm transition-all hover:scale-105"
|
||||
>
|
||||
Accepteren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Add to window type
|
||||
declare global {
|
||||
interface Window {
|
||||
plausible: (event: string, options?: Record<string, unknown>) => void;
|
||||
}
|
||||
}
|
||||
@@ -53,59 +53,71 @@ const artForms = [
|
||||
|
||||
export default function ArtForms() {
|
||||
return (
|
||||
<section className="snap-section relative z-25 min-h-screen w-full bg-[#f8f8f8] px-12 py-16">
|
||||
<section className="relative z-25 w-full bg-[#f8f8f8] px-12 py-16">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<h2 className="mb-4 font-['Intro',sans-serif] text-5xl text-[#214e51]">
|
||||
Kies Je Traject
|
||||
</h2>
|
||||
<p className="mb-12 max-w-2xl font-['Intro',sans-serif] text-gray-600 text-xl">
|
||||
<p className="mb-12 max-w-2xl text-gray-600 text-xl">
|
||||
Kunstenkamp biedt trajecten aan voor verschillende kunstvormen. Ontdek
|
||||
waar jouw passie ligt en ontwikkel je talent onder begeleiding van
|
||||
ervaringsdeskundigen.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{artForms.map((art) => {
|
||||
{artForms.map((art, index) => {
|
||||
const IconComponent = art.icon;
|
||||
return (
|
||||
<div
|
||||
<article
|
||||
key={art.title}
|
||||
className="group relative overflow-hidden bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl"
|
||||
className="group relative overflow-hidden bg-white p-8 transition-all focus-within:ring-2 focus-within:ring-[#214e51] focus-within:ring-offset-2 hover:-translate-y-2 hover:shadow-xl motion-reduce:transition-none"
|
||||
aria-labelledby={`art-title-${index}`}
|
||||
>
|
||||
{/* Color bar at top */}
|
||||
<div
|
||||
className="absolute top-0 left-0 h-1 w-full transition-all group-hover:h-2"
|
||||
className="absolute top-0 left-0 h-1 w-full transition-all group-hover:h-2 motion-reduce:transition-none"
|
||||
style={{ backgroundColor: art.color }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div
|
||||
className="flex h-14 w-14 items-center justify-center"
|
||||
style={{ backgroundColor: art.color }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<IconComponent className="h-7 w-7 text-white" />
|
||||
<IconComponent
|
||||
className="h-7 w-7 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-['Intro',sans-serif] text-2xl text-gray-900">
|
||||
<h3
|
||||
id={`art-title-${index}`}
|
||||
className="font-['Intro',sans-serif] text-2xl text-gray-900"
|
||||
>
|
||||
{art.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="mb-6 font-['Intro',sans-serif] text-gray-600 leading-relaxed">
|
||||
<p className="mb-6 text-gray-600 leading-relaxed">
|
||||
{art.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between border-gray-100 border-t pt-4">
|
||||
<span
|
||||
className="font-['Intro',sans-serif] font-medium text-sm"
|
||||
className="font-medium text-sm"
|
||||
style={{ color: art.color }}
|
||||
>
|
||||
{art.trajectory}
|
||||
</span>
|
||||
<span className="text-2xl text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-600">
|
||||
<span
|
||||
className="text-2xl text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-600 motion-reduce:transition-none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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<FormErrors>({});
|
||||
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
||||
|
||||
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
const newValue = type === "checkbox" ? checked : value;
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<section
|
||||
id="registration"
|
||||
className="snap-section relative z-30 min-h-screen w-full bg-[#214e51]/96 px-12 py-16"
|
||||
className="relative z-30 flex w-full items-center justify-center bg-[#214e51]/96 px-6 py-16 md:px-12"
|
||||
>
|
||||
<div className="mx-auto flex h-full max-w-6xl flex-col">
|
||||
<h2 className="mb-2 font-['Intro',sans-serif] text-4xl text-white">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col">
|
||||
<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 font-['Intro',sans-serif] text-white/80 text-xl">
|
||||
Doet dit jouw creatieve geest borellen? Vul nog even dit formulier in
|
||||
<p className="mb-12 max-w-3xl text-lg text-white/80 md:text-xl">
|
||||
Doet dit jouw creatieve geest borrelen? Vul nog even dit formulier in
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||
<div className="flex flex-col gap-8">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col"
|
||||
noValidate
|
||||
>
|
||||
<div className="flex flex-col gap-6 md:gap-8">
|
||||
{/* Row 1: First Name + Last Name */}
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="firstName"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Voornaam
|
||||
Voornaam <span className="text-red-300">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -84,18 +225,33 @@ export default function EventRegistrationForm() {
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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"
|
||||
autoComplete="given-name"
|
||||
aria-required="true"
|
||||
aria-invalid={touched.firstName && !!errors.firstName}
|
||||
aria-describedby={
|
||||
errors.firstName ? "firstName-error" : undefined
|
||||
}
|
||||
className={getInputClasses("firstName")}
|
||||
/>
|
||||
{touched.firstName && errors.firstName && (
|
||||
<span
|
||||
id="firstName-error"
|
||||
className="text-red-300 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
{errors.firstName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="lastName"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Achternaam
|
||||
Achternaam <span className="text-red-300">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -103,21 +259,36 @@ export default function EventRegistrationForm() {
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
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"
|
||||
autoComplete="family-name"
|
||||
aria-required="true"
|
||||
aria-invalid={touched.lastName && !!errors.lastName}
|
||||
aria-describedby={
|
||||
errors.lastName ? "lastName-error" : undefined
|
||||
}
|
||||
className={getInputClasses("lastName")}
|
||||
/>
|
||||
{touched.lastName && errors.lastName && (
|
||||
<span
|
||||
id="lastName-error"
|
||||
className="text-red-300 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
{errors.lastName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Email + Phone */}
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
E-mail
|
||||
E-mail <span className="text-red-300">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -125,16 +296,30 @@ export default function EventRegistrationForm() {
|
||||
name="email"
|
||||
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"
|
||||
onBlur={handleBlur}
|
||||
placeholder="jouw@email.be"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
aria-required="true"
|
||||
aria-invalid={touched.email && !!errors.email}
|
||||
aria-describedby={errors.email ? "email-error" : undefined}
|
||||
className={getInputClasses("email")}
|
||||
/>
|
||||
{touched.email && errors.email && (
|
||||
<span
|
||||
id="email-error"
|
||||
className="text-red-300 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
{errors.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Telefoon
|
||||
</label>
|
||||
@@ -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 && (
|
||||
<span
|
||||
id="phone-error"
|
||||
className="text-red-300 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
{errors.phone}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Art Form (full width) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="artForm"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
>
|
||||
Kunstvorm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="artForm"
|
||||
name="artForm"
|
||||
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>
|
||||
|
||||
{/* Row 4: Experience (full width) */}
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="experience"
|
||||
className="font-['Intro',sans-serif] text-2xl text-white"
|
||||
>
|
||||
Ervaring
|
||||
</label>
|
||||
{/* Row 3: Wants to perform checkbox */}
|
||||
<label
|
||||
htmlFor="wantsToPerform"
|
||||
className="flex cursor-pointer items-center gap-4"
|
||||
>
|
||||
<div className="relative flex shrink-0 self-center">
|
||||
<input
|
||||
type="text"
|
||||
id="experience"
|
||||
name="experience"
|
||||
value={formData.experience}
|
||||
type="checkbox"
|
||||
id="wantsToPerform"
|
||||
name="wantsToPerform"
|
||||
checked={formData.wantsToPerform}
|
||||
onChange={handleChange}
|
||||
placeholder="Beginner / Gevorderd / Professional"
|
||||
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"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-6 border-2 border-white/50 bg-transparent transition-colors peer-checked:border-white peer-checked:bg-white peer-focus:ring-2 peer-focus:ring-white/50 peer-focus:ring-offset-2 peer-focus:ring-offset-[#214e51]" />
|
||||
<svg
|
||||
className="pointer-events-none absolute top-0 left-0 h-6 w-6 scale-0 text-[#214e51] transition-transform peer-checked:scale-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-white text-xl md:text-2xl">
|
||||
Ik wil optreden
|
||||
</span>
|
||||
<span className="text-sm text-white/60">
|
||||
Vink aan als je wilt optreden. Laat dit leeg als je alleen
|
||||
wilt komen kijken.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Performer fields: shown only when wantsToPerform is checked */}
|
||||
{formData.wantsToPerform && (
|
||||
<div className="flex flex-col gap-6 border border-white/20 p-6 md:gap-8">
|
||||
{/* Art Form */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="artForm"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Kunstvorm <span className="text-red-300">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="artForm"
|
||||
name="artForm"
|
||||
value={formData.artForm}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Muziek, Theater, Dans, etc."
|
||||
autoComplete="off"
|
||||
list="artFormSuggestions"
|
||||
aria-required="true"
|
||||
aria-invalid={touched.artForm && !!errors.artForm}
|
||||
aria-describedby={
|
||||
errors.artForm ? "artForm-error" : undefined
|
||||
}
|
||||
className={getInputClasses("artForm")}
|
||||
/>
|
||||
<datalist id="artFormSuggestions">
|
||||
<option value="Muziek" />
|
||||
<option value="Theater" />
|
||||
<option value="Dans" />
|
||||
<option value="Beeldende Kunst" />
|
||||
<option value="Woordkunst" />
|
||||
<option value="Comedy" />
|
||||
</datalist>
|
||||
{touched.artForm && errors.artForm && (
|
||||
<span
|
||||
id="artForm-error"
|
||||
className="text-red-300 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
{errors.artForm}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Experience */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="experience"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Ervaring
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="experience"
|
||||
name="experience"
|
||||
value={formData.experience}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Beginner / Gevorderd / Professional"
|
||||
autoComplete="off"
|
||||
list="experienceSuggestions"
|
||||
className={getInputClasses("experience")}
|
||||
/>
|
||||
<datalist id="experienceSuggestions">
|
||||
<option value="Beginner" />
|
||||
<option value="Gevorderd" />
|
||||
<option value="Professional" />
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extra questions / remarks */}
|
||||
<div className="flex flex-col gap-2 border border-white/20 p-6">
|
||||
<label
|
||||
htmlFor="extraQuestions"
|
||||
className="text-white text-xl md:text-2xl"
|
||||
>
|
||||
Vragen of opmerkingen
|
||||
</label>
|
||||
<textarea
|
||||
id="extraQuestions"
|
||||
name="extraQuestions"
|
||||
value={formData.extraQuestions}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Heb je nog vragen of iets dat we moeten weten?"
|
||||
rows={4}
|
||||
autoComplete="off"
|
||||
className="resize-none bg-transparent text-lg text-white transition-all placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-[#214e51]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,17 +489,45 @@ export default function EventRegistrationForm() {
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
aria-busy={submitMutation.isPending}
|
||||
className="bg-white px-12 py-4 font-['Intro',sans-serif] text-[#214e51] text-xl transition-all hover:scale-105 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-[#214e51] disabled:cursor-not-allowed disabled:opacity-50 md:text-2xl"
|
||||
>
|
||||
{submitMutation.isPending ? "Bezig..." : "Bevestigen"}
|
||||
{submitMutation.isPending ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Laden"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
Bezig...
|
||||
</span>
|
||||
) : (
|
||||
"Bevestigen"
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-center font-['Intro',sans-serif] text-sm text-white/60">
|
||||
Nu al een act / idee van wat jij graag zou willen brengen,{" "}
|
||||
<span className="cursor-pointer underline hover:text-white">
|
||||
klik HIER
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
href="/contact"
|
||||
className="link-hover block text-center text-sm text-white/60 transition-colors hover:text-white"
|
||||
>
|
||||
Nog vragen? Neem contact op
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function Footer() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className="snap-section relative z-40 flex h-[250px] flex-col items-center justify-center bg-[#d09035]">
|
||||
<footer className="relative z-40 flex h-[250px] flex-col items-center justify-center bg-[#d09035]">
|
||||
<div className="text-center">
|
||||
<h3 className="mb-4 font-['Intro',sans-serif] text-2xl text-white">
|
||||
Kunstenkamp
|
||||
@@ -26,33 +26,47 @@ export default function Footer() {
|
||||
Waar creativiteit tot leven komt
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-8 font-['Intro',sans-serif] text-sm text-white/70">
|
||||
<a href="/privacy" className="transition-colors hover:text-white">
|
||||
<div className="flex items-center justify-center gap-8 text-sm text-white/70">
|
||||
<a
|
||||
href="/privacy"
|
||||
className="link-hover transition-colors hover:text-white"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
<span className="text-white/40">|</span>
|
||||
<a href="/terms" className="transition-colors hover:text-white">
|
||||
<a
|
||||
href="/terms"
|
||||
className="link-hover transition-colors hover:text-white"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
<span className="text-white/40">|</span>
|
||||
<a href="/contact" className="transition-colors hover:text-white">
|
||||
<a
|
||||
href="/contact"
|
||||
className="link-hover transition-colors hover:text-white"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
{!isLoading && isAdmin && (
|
||||
<>
|
||||
<span className="text-white/40">|</span>
|
||||
<Link to="/admin" className="transition-colors hover:text-white">
|
||||
<Link
|
||||
to="/admin"
|
||||
className="link-hover transition-colors hover:text-white"
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 font-['Intro',sans-serif] text-white/50 text-xs">
|
||||
© 2026 Kunstenkamp. Alle rechten voorbehouden.
|
||||
<div className="mt-6 text-white/50 text-xs">
|
||||
© {new Date().getFullYear()} Kunstenkamp. Alle rechten voorbehouden.
|
||||
</div>
|
||||
<div className="font-['Intro',sans-serif] text-white/50 text-xs">
|
||||
<a href="https://zias.be">Gemaakt met ♥ door zias.be</a>
|
||||
<div className="text-white/50 text-xs transition-colors hover:text-white">
|
||||
<a href="https://zias.be" className="link-hover">
|
||||
Gemaakt met ♥ door zias.be
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -12,25 +12,22 @@ export default function Hero() {
|
||||
}, []);
|
||||
|
||||
const scrollToRegistration = () => {
|
||||
const registrationSection = document.getElementById("registration");
|
||||
if (registrationSection) {
|
||||
// Temporarily disable scroll-snap to prevent snap-back on mobile
|
||||
document.documentElement.style.scrollSnapType = "none";
|
||||
registrationSection.scrollIntoView({ behavior: "smooth" });
|
||||
// Re-enable scroll-snap after smooth scroll completes
|
||||
setTimeout(() => {
|
||||
document.documentElement.style.scrollSnapType = "";
|
||||
}, 800);
|
||||
}
|
||||
document
|
||||
.getElementById("registration")
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const scrollToInfo = () => {
|
||||
document.getElementById("info")?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="snap-section relative h-[100dvh] w-full overflow-hidden bg-white">
|
||||
<section className="relative h-[100dvh] w-full overflow-hidden bg-white">
|
||||
{/* Desktop Layout - hidden on mobile */}
|
||||
<div className="relative hidden h-full w-full flex-col gap-[10px] p-[10px] lg:flex">
|
||||
{/* Desktop Microphone - positioned on top of top sections, under bottom sections */}
|
||||
<div
|
||||
className={`pointer-events-none absolute z-20 transition-all duration-1000 ease-out ${
|
||||
className={`pointer-events-none absolute z-20 transition-all duration-1000 ease-out motion-reduce:transition-none ${
|
||||
micVisible
|
||||
? "translate-x-0 translate-y-0 opacity-100"
|
||||
: "translate-x-24 -translate-y-24 opacity-0"
|
||||
@@ -43,8 +40,10 @@ export default function Hero() {
|
||||
>
|
||||
<img
|
||||
src="/assets/mic.png"
|
||||
alt="Vintage microphone"
|
||||
alt="Vintage microfoon - Open Mic Night"
|
||||
className="h-full w-full object-contain drop-shadow-2xl"
|
||||
loading="eager"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -57,9 +56,13 @@ export default function Hero() {
|
||||
<br />
|
||||
NIGHT
|
||||
</h1>
|
||||
<p className="mt-4 font-['Intro',sans-serif] font-normal text-[clamp(1.25rem,2.5vw,2.625rem)] text-white/90">
|
||||
<button
|
||||
onClick={scrollToInfo}
|
||||
type="button"
|
||||
className="link-hover mt-4 cursor-pointer text-left font-['Intro',sans-serif] font-normal text-[clamp(1.25rem,2.5vw,2.625rem)] text-white/70 transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
Ongedesemd brood
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Top Right - Teal - under mic */}
|
||||
@@ -88,13 +91,13 @@ export default function Hero() {
|
||||
</div>
|
||||
|
||||
{/* Desktop CTA Button - centered at bottom */}
|
||||
<div className="absolute bottom-[10px] left-1/2 z-30 -translate-x-1/2">
|
||||
<div className="absolute bottom-[10px] left-1/2 z-30 -translate-x-1/2 py-12">
|
||||
<button
|
||||
onClick={scrollToRegistration}
|
||||
type="button"
|
||||
className="bg-black px-[40px] py-[10px] font-['Intro',sans-serif] font-normal text-[30px] text-white transition-all hover:scale-105 hover:bg-gray-900"
|
||||
>
|
||||
Registreer nu!
|
||||
Registreer je nu!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +119,7 @@ export default function Hero() {
|
||||
|
||||
{/* Mobile Microphone - positioned inside magenta section, clipped by overflow */}
|
||||
<div
|
||||
className={`pointer-events-none absolute z-10 transition-all duration-1000 ease-out ${
|
||||
className={`pointer-events-none absolute z-10 transition-all duration-1000 ease-out motion-reduce:transition-none ${
|
||||
micVisible
|
||||
? "translate-x-0 translate-y-0 opacity-100"
|
||||
: "translate-x-12 -translate-y-12 opacity-0"
|
||||
@@ -130,8 +133,9 @@ export default function Hero() {
|
||||
>
|
||||
<img
|
||||
src="/assets/mic.png"
|
||||
alt="Vintage microphone"
|
||||
alt="Vintage microfoon - Open Mic Night"
|
||||
className="h-full w-full object-contain object-bottom drop-shadow-2xl"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +166,7 @@ export default function Hero() {
|
||||
type="button"
|
||||
className="w-full bg-black py-3 font-['Intro',sans-serif] font-normal text-lg text-white transition-all hover:scale-105 hover:bg-gray-900"
|
||||
>
|
||||
Registreer nu!
|
||||
Registreer je nu!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const questions = [
|
||||
const faqQuestions = [
|
||||
{
|
||||
question: "Wat is een open mic?",
|
||||
answer:
|
||||
@@ -23,23 +23,151 @@ const questions = [
|
||||
|
||||
export default function Info() {
|
||||
return (
|
||||
<section className="snap-section relative z-20 flex min-h-screen flex-col bg-[#d82560]/96 px-12 py-16">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col justify-center gap-16">
|
||||
{questions.map((item, idx) => (
|
||||
<div
|
||||
key={item.question}
|
||||
className={`flex flex-col gap-4 ${
|
||||
idx % 2 === 0 ? "items-start text-left" : "items-end text-right"
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-['Intro',sans-serif] text-4xl text-white">
|
||||
{item.question}
|
||||
</h3>
|
||||
<p className="max-w-xl font-['Intro',sans-serif] text-white/80 text-xl">
|
||||
{item.answer}
|
||||
</p>
|
||||
<section id="info" className="relative z-20 flex flex-col bg-[#d82560]/96">
|
||||
{/* Hero Section - Ongedesemd Brood */}
|
||||
<div className="relative w-full border-white/20 border-b-4">
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className="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 flex flex-col items-center justify-center px-8 py-12 text-center">
|
||||
{/* Decorative top element */}
|
||||
<div className="mb-8 flex items-center gap-4">
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-white/60 to-transparent" />
|
||||
<svg
|
||||
className="h-14 w-20 text-white/80"
|
||||
viewBox="0 0 80 56"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Ongedesemd brood"
|
||||
>
|
||||
{/* Flat matzo cracker - slightly rounded rect */}
|
||||
<rect
|
||||
x="2"
|
||||
y="8"
|
||||
width="76"
|
||||
height="40"
|
||||
rx="4"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/* Perforation dots - characteristic matzo pattern */}
|
||||
{[16, 28, 40, 52, 64].map((x) =>
|
||||
[18, 28, 38].map((y) => (
|
||||
<circle
|
||||
key={`${x}-${y}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r="2"
|
||||
fill="rgba(0,0,0,0.25)"
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
{/* Score lines */}
|
||||
<line
|
||||
x1="2"
|
||||
y1="22"
|
||||
x2="78"
|
||||
y2="22"
|
||||
stroke="rgba(0,0,0,0.15)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1="2"
|
||||
y1="34"
|
||||
x2="78"
|
||||
y2="34"
|
||||
stroke="rgba(0,0,0,0.15)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</svg>
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-white/60 to-transparent" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Main Title */}
|
||||
<h2 className="font-['Intro',sans-serif] font-black text-6xl text-white uppercase tracking-tight md:text-8xl lg:text-9xl">
|
||||
Ongedesemd
|
||||
<span className="block text-5xl md:text-7xl lg:text-8xl">
|
||||
Brood?!
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Subtitle / Tagline */}
|
||||
<p className="mt-8 max-w-3xl font-['Intro',sans-serif] font-light text-2xl text-white/90 uppercase tracking-widest md:text-3xl">
|
||||
Kunst zonder franje · Puur vanuit het hart
|
||||
</p>
|
||||
|
||||
{/* Decorative bottom element */}
|
||||
<div className="mt-10 flex items-center gap-3">
|
||||
<div className="h-2 w-2 rotate-45 bg-white/60" />
|
||||
<div className="h-2 w-2 rotate-45 bg-white/40" />
|
||||
<div className="h-2 w-2 rotate-45 bg-white/60" />
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
{/* 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" />
|
||||
|
||||
<h3 className="font-['Intro',sans-serif] text-3xl text-white md:text-4xl">
|
||||
Waarom deze naam?
|
||||
</h3>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-lg text-white/90 leading-relaxed">
|
||||
In de Bijbel staat ongedesemd brood (ook wel{" "}
|
||||
<em className="text-white">matze</em> genoemd) symbool voor
|
||||
eenvoud en zuiverheid, zonder de 'ballast' van
|
||||
desem.
|
||||
</p>
|
||||
<p className="text-lg text-white/80 leading-relaxed">
|
||||
Het werd gegeten tijdens Pesach als herinnering aan de
|
||||
haastige uittocht uit Egypte, toen er geen tijd was om brood
|
||||
te laten rijzen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-lg text-white/80 leading-relaxed">
|
||||
Bij ons staat het voor kunst zonder franje: geen ingewikkelde
|
||||
regels of hoge drempels, gewoon authentieke expressie vanuit
|
||||
het hart.
|
||||
</p>
|
||||
<div className="rounded-xl border border-white/30 bg-white/10 p-6">
|
||||
<p className="font-['Intro',sans-serif] font-semibold text-white text-xl">
|
||||
Kom vooral ook gewoon kijken!
|
||||
</p>
|
||||
<p className="mt-2 text-white/80">
|
||||
Je hoeft absoluut niet te performen om te genieten van een
|
||||
inspirerende avond vol muziek, poëzie en andere kunst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
{faqQuestions.map((item, idx) => (
|
||||
<div
|
||||
key={item.question}
|
||||
className={`flex flex-col gap-4 ${
|
||||
idx % 2 === 0 ? "items-start text-left" : "items-end text-right"
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-['Intro',sans-serif] text-4xl text-white">
|
||||
{item.question}
|
||||
</h3>
|
||||
<p className="max-w-xl text-white/80 text-xl">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user