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:
6
apps/web/public/favicon.svg
Normal file
6
apps/web/public/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#d82560"/>
|
||||
<circle cx="50" cy="35" r="15" fill="white"/>
|
||||
<rect x="40" y="45" width="20" height="30" fill="white"/>
|
||||
<rect x="35" y="75" width="30" height="8" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
BIN
apps/web/public/fonts/DMSans-latin-ext.woff2
Normal file
BIN
apps/web/public/fonts/DMSans-latin-ext.woff2
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/DMSans-latin.woff2
Normal file
BIN
apps/web/public/fonts/DMSans-latin.woff2
Normal file
Binary file not shown.
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>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* DM Sans font family - Self hosted */
|
||||
@font-face {
|
||||
font-family: "DM Sans";
|
||||
src: url("/fonts/DMSans-latin.woff2") format("woff2");
|
||||
font-weight: 400 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DM Sans";
|
||||
src: url("/fonts/DMSans-latin-ext.woff2") format("woff2");
|
||||
font-weight: 400 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range:
|
||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* Intro font family - Self hosted */
|
||||
@font-face {
|
||||
font-family: "Intro";
|
||||
@@ -21,11 +42,54 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Font theme tokens */
|
||||
@theme {
|
||||
--font-body: "DM Sans", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
/* Link hover underline animation */
|
||||
@utility link-hover {
|
||||
@apply relative after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 after:bg-current after:transition-[width] after:duration-300 hover:after:w-full;
|
||||
}
|
||||
|
||||
@utility link-hover {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background-color: currentColor;
|
||||
transition: width 300ms;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling for the entire page */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Respect reduced motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms;
|
||||
animation-iteration-count: 1;
|
||||
transition-duration: 0.01ms;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
@@ -45,31 +109,25 @@ html {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Sticky scroll container with snap points */
|
||||
.sticky-scroll-container {
|
||||
height: 400vh; /* 4 sections x 100vh */
|
||||
position: relative;
|
||||
/* ─── Text Selection Colors ─────────────────────────────────────────────────
|
||||
Each section gets a ::selection style that harmonizes with its background.
|
||||
The goal: selection feels native to each section, not a browser default.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Default / White backgrounds (Hero, ArtForms) */
|
||||
::selection {
|
||||
background-color: #d82560;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sticky-section {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
/* Info section — magenta background (#d82560/96) */
|
||||
#info ::selection {
|
||||
background-color: #ffffff;
|
||||
color: #d82560;
|
||||
}
|
||||
|
||||
/* Scroll snap for the whole page - mandatory snapping to closest section */
|
||||
html {
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.snap-section {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
/* Ensure snap works consistently in both directions */
|
||||
.snap-section {
|
||||
scroll-margin-top: 0;
|
||||
/* Registration section — dark teal background (#214e51/96) */
|
||||
#registration ::selection {
|
||||
background-color: #d09035;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
@@ -9,17 +9,35 @@
|
||||
// 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 TermsRouteImport } from './routes/terms'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
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 ApiRpcSplatRouteImport } from './routes/api/rpc/$'
|
||||
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
|
||||
|
||||
const TermsRoute = TermsRouteImport.update({
|
||||
id: '/terms',
|
||||
path: '/terms',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PrivacyRoute = PrivacyRouteImport.update({
|
||||
id: '/privacy',
|
||||
path: '/privacy',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ContactRoute = ContactRouteImport.update({
|
||||
id: '/contact',
|
||||
path: '/contact',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminRoute = AdminRouteImport.update({
|
||||
id: '/admin',
|
||||
path: '/admin',
|
||||
@@ -44,14 +62,20 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/admin': typeof AdminRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/admin': typeof AdminRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||
}
|
||||
@@ -59,28 +83,73 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/admin': typeof AdminRoute
|
||||
'/contact': typeof ContactRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatRoute
|
||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/admin'
|
||||
| '/contact'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/api/auth/$'
|
||||
| '/api/rpc/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
|
||||
id: '__root__' | '/' | '/admin' | '/login' | '/api/auth/$' | '/api/rpc/$'
|
||||
to:
|
||||
| '/'
|
||||
| '/admin'
|
||||
| '/contact'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/api/auth/$'
|
||||
| '/api/rpc/$'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/admin'
|
||||
| '/contact'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/api/auth/$'
|
||||
| '/api/rpc/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AdminRoute: typeof AdminRoute
|
||||
ContactRoute: typeof ContactRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
TermsRoute: typeof TermsRoute
|
||||
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
||||
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/terms': {
|
||||
id: '/terms'
|
||||
path: '/terms'
|
||||
fullPath: '/terms'
|
||||
preLoaderRoute: typeof TermsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/privacy': {
|
||||
id: '/privacy'
|
||||
path: '/privacy'
|
||||
fullPath: '/privacy'
|
||||
preLoaderRoute: typeof PrivacyRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
@@ -88,6 +157,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/contact': {
|
||||
id: '/contact'
|
||||
path: '/contact'
|
||||
fullPath: '/contact'
|
||||
preLoaderRoute: typeof ContactRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin': {
|
||||
id: '/admin'
|
||||
path: '/admin'
|
||||
@@ -122,7 +198,10 @@ declare module '@tanstack/react-router' {
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AdminRoute: AdminRoute,
|
||||
ContactRoute: ContactRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
TermsRoute: TermsRoute,
|
||||
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
||||
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
||||
}
|
||||
|
||||
@@ -8,10 +8,18 @@ import {
|
||||
Scripts,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { CookieConsent } from "@/components/CookieConsent";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { orpc } from "@/utils/orpc";
|
||||
|
||||
import appCss from "../index.css?url";
|
||||
|
||||
const siteUrl = "https://kunstenkamp.be";
|
||||
const siteTitle = "Kunstenkamp Open Mic Night - Ongedesemd Brood";
|
||||
const siteDescription =
|
||||
"Doe mee met de Open Mic Night op 18 april! Een avond vol muziek, theater, dans, woordkunst en meer. Iedereen is welkom - van beginner tot professional.";
|
||||
const eventImage = `${siteUrl}/assets/og-image.jpg`;
|
||||
|
||||
export interface RouterAppContext {
|
||||
orpc: typeof orpc;
|
||||
queryClient: QueryClient;
|
||||
@@ -28,7 +36,57 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "My App",
|
||||
title: siteTitle,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: siteDescription,
|
||||
},
|
||||
{
|
||||
name: "theme-color",
|
||||
content: "#d82560",
|
||||
},
|
||||
// Open Graph
|
||||
{
|
||||
property: "og:type",
|
||||
content: "website",
|
||||
},
|
||||
{
|
||||
property: "og:url",
|
||||
content: siteUrl,
|
||||
},
|
||||
{
|
||||
property: "og:title",
|
||||
content: siteTitle,
|
||||
},
|
||||
{
|
||||
property: "og:description",
|
||||
content: siteDescription,
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: eventImage,
|
||||
},
|
||||
{
|
||||
property: "og:locale",
|
||||
content: "nl_BE",
|
||||
},
|
||||
// Twitter Card
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image",
|
||||
},
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: siteTitle,
|
||||
},
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: siteDescription,
|
||||
},
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: eventImage,
|
||||
},
|
||||
],
|
||||
links: [
|
||||
@@ -36,17 +94,58 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
{
|
||||
rel: "canonical",
|
||||
href: siteUrl,
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/svg+xml",
|
||||
href: "/favicon.svg",
|
||||
},
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://analytics.zias.be",
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
{
|
||||
src: "https://analytics.zias.be/js/script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js",
|
||||
defer: true,
|
||||
"data-domain": "kunstenkamp.be",
|
||||
"data-api": "https://analytics.zias.be/api/event",
|
||||
},
|
||||
{
|
||||
children:
|
||||
"window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }",
|
||||
},
|
||||
// JSON-LD Schema for Event
|
||||
{
|
||||
type: "application/ld+json",
|
||||
children: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Event",
|
||||
name: "Kunstenkamp Open Mic Night",
|
||||
description:
|
||||
"Een avond vol muziek, theater, dans, woordkunst en meer. Iedereen is welkom om zijn of haar talent te tonen.",
|
||||
startDate: "2026-04-18T19:00:00+02:00",
|
||||
eventStatus: "https://schema.org/EventScheduled",
|
||||
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
|
||||
organizer: {
|
||||
"@type": "Organization",
|
||||
name: "Kunstenkamp",
|
||||
url: siteUrl,
|
||||
},
|
||||
image: eventImage,
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: "0",
|
||||
priceCurrency: "EUR",
|
||||
availability: "https://schema.org/InStock",
|
||||
validFrom: "2026-03-01T00:00:00+02:00",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -55,13 +154,22 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||
|
||||
function RootDocument() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<a
|
||||
href="#main-content"
|
||||
className="fixed top-0 left-0 z-[100] -translate-y-full bg-[#d82560] px-4 py-2 text-white transition-transform focus:translate-y-0"
|
||||
>
|
||||
Ga naar hoofdinhoud
|
||||
</a>
|
||||
<div id="main-content">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Toaster />
|
||||
<CookieConsent />
|
||||
<TanStackRouterDevtools position="bottom-left" />
|
||||
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
|
||||
<Scripts />
|
||||
|
||||
@@ -250,9 +250,7 @@ function AdminPage() {
|
||||
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>
|
||||
<span className="text-white">{item.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
65
apps/web/src/routes/contact.tsx
Normal file
65
apps/web/src/routes/contact.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/contact")({
|
||||
component: ContactPage,
|
||||
});
|
||||
|
||||
function ContactPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link
|
||||
to="/"
|
||||
className="link-hover mb-8 inline-block text-white/80 hover:text-white"
|
||||
>
|
||||
← Terug naar home
|
||||
</Link>
|
||||
<h1 className="mb-8 font-['Intro',sans-serif] text-4xl text-white">
|
||||
Contact
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6 text-white/80">
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Heb je vragen?</h2>
|
||||
<p className="mb-4">
|
||||
We helpen je graag! Je kunt ons bereiken via de volgende kanalen:
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg bg-white/5 p-6">
|
||||
<h3 className="mb-3 text-white text-xl">E-mail</h3>
|
||||
<a
|
||||
href="mailto:info@kunstenkamp.be"
|
||||
className="link-hover text-white/80 hover:text-white"
|
||||
>
|
||||
info@kunstenkamp.be
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg bg-white/5 p-6">
|
||||
<h3 className="mb-3 text-white text-xl">Open Mic Night</h3>
|
||||
<p>Vrijdag 18 april 2026</p>
|
||||
<p>Aanvang: 19:00 uur</p>
|
||||
<p className="mt-2 text-white/60">
|
||||
Locatie wordt later bekendgemaakt aan geregistreerde deelnemers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg bg-white/5 p-6">
|
||||
<h3 className="mb-3 text-white text-xl">Organisatie</h3>
|
||||
<p>Kunstenkamp vzw</p>
|
||||
<p className="mt-2 text-white/60">
|
||||
Een initiatief voor en door kunstenaars.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-8">
|
||||
<p className="text-sm text-white/60">
|
||||
We proberen je e-mail binnen 48 uur te beantwoorden.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ function LoginPage() {
|
||||
};
|
||||
|
||||
const handleRequestAdmin = () => {
|
||||
requestAdminMutation.mutate();
|
||||
requestAdminMutation.mutate(undefined);
|
||||
};
|
||||
|
||||
// If already logged in as admin, redirect
|
||||
|
||||
75
apps/web/src/routes/privacy.tsx
Normal file
75
apps/web/src/routes/privacy.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/privacy")({
|
||||
component: PrivacyPage,
|
||||
});
|
||||
|
||||
function PrivacyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link
|
||||
to="/"
|
||||
className="link-hover mb-8 inline-block text-white/80 hover:text-white"
|
||||
>
|
||||
← Terug naar home
|
||||
</Link>
|
||||
<h1 className="mb-8 font-['Intro',sans-serif] text-4xl text-white">
|
||||
Privacybeleid
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6 text-white/80">
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">
|
||||
Welke gegevens verzamelen we?
|
||||
</h2>
|
||||
<p>
|
||||
We verzamelen alleen de gegevens die je zelf invoert bij de
|
||||
registratie: voornaam, achternaam, e-mailadres, telefoonnummer,
|
||||
kunstvorm en ervaring.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">
|
||||
Waarom verzamelen we deze gegevens?
|
||||
</h2>
|
||||
<p>
|
||||
We gebruiken je gegevens om je te kunnen contacteren over de Open
|
||||
Mic Night, om het programma samen te stellen en om je te
|
||||
informeren over aanvullende details over het evenement.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">
|
||||
Hoe lang bewaren we je gegevens?
|
||||
</h2>
|
||||
<p>
|
||||
Je gegevens worden bewaard tot na het evenement en maximaal 6
|
||||
maanden daarna, tenzij je vraagt om eerder verwijdering.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Je rechten</h2>
|
||||
<p>
|
||||
Je hebt het recht om je gegevens in te zien, te corrigeren of te
|
||||
laten verwijderen. Neem hiervoor contact op via
|
||||
info@kunstenkamp.be.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Cookies</h2>
|
||||
<p>
|
||||
We gebruiken alleen analytische cookies om het gebruik van onze
|
||||
website te verbeteren. Deze cookies bevatten geen persoonlijke
|
||||
informatie.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
apps/web/src/routes/terms.tsx
Normal file
71
apps/web/src/routes/terms.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/terms")({
|
||||
component: TermsPage,
|
||||
});
|
||||
|
||||
function TermsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<Link
|
||||
to="/"
|
||||
className="link-hover mb-8 inline-block text-white/80 hover:text-white"
|
||||
>
|
||||
← Terug naar home
|
||||
</Link>
|
||||
<h1 className="mb-8 font-['Intro',sans-serif] text-4xl text-white">
|
||||
Algemene Voorwaarden
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6 text-white/80">
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">
|
||||
Deelname aan Open Mic Night
|
||||
</h2>
|
||||
<p>
|
||||
Door je te registreren voor de Open Mic Night ga je akkoord met
|
||||
deze voorwaarden. Je krijgt 5-7 minuten podiumtijd, afhankelijk
|
||||
van het aantal deelnemers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Annulering</h2>
|
||||
<p>
|
||||
Als je niet kunt komen, geef dit dan minimaal 48 uur van tevoren
|
||||
door. Zo kunnen we iemand anders de kans geven om op te treden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Gedragsregels</h2>
|
||||
<p>
|
||||
We verwachten van alle deelnemers dat ze respectvol omgaan met
|
||||
elkaar, het publiek en het materiaal. Discriminatie, intimidatie
|
||||
of agressief gedrag worden niet getolereerd.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Foto's en video's</h2>
|
||||
<p>
|
||||
Tijdens het evenement kunnen foto's en video's worden gemaakt voor
|
||||
promotionele doeleinden. Door deel te nemen ga je hiermee akkoord,
|
||||
tenziij je dit expliciet aangeeft bij de organisatie.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-2xl text-white">Aansprakelijkheid</h2>
|
||||
<p>
|
||||
Kunstenkamp is niet aansprakelijk voor verlies of beschadiging van
|
||||
persoonlijke eigendommen. Je bent zelf verantwoordelijk voor je
|
||||
eigen materiaal en instrumenten.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { db } from "@kk/db";
|
||||
import { adminRequest, registration } from "@kk/db/schema";
|
||||
import { user } from "@kk/db/schema/auth";
|
||||
import type { RouterClient } from "@orpc/server";
|
||||
import { randomUUID } from "crypto";
|
||||
import { and, count, desc, eq, gte, like, lte } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
|
||||
@@ -12,8 +12,10 @@ const submitRegistrationSchema = z.object({
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phone: z.string().optional(),
|
||||
artForm: z.string().min(1),
|
||||
wantsToPerform: z.boolean().default(false),
|
||||
artForm: z.string().optional(),
|
||||
experience: z.string().optional(),
|
||||
extraQuestions: z.string().optional(),
|
||||
});
|
||||
|
||||
const getRegistrationsSchema = z.object({
|
||||
@@ -46,8 +48,10 @@ export const appRouter = {
|
||||
lastName: input.lastName,
|
||||
email: input.email,
|
||||
phone: input.phone || null,
|
||||
artForm: input.artForm,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
artForm: input.artForm || null,
|
||||
experience: input.experience || null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
});
|
||||
|
||||
return { success: true, id: result.lastInsertRowid };
|
||||
@@ -149,8 +153,10 @@ export const appRouter = {
|
||||
"Last Name",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Wants To Perform",
|
||||
"Art Form",
|
||||
"Experience",
|
||||
"Extra Questions",
|
||||
"Created At",
|
||||
];
|
||||
const rows = data.map((r) => [
|
||||
@@ -159,8 +165,10 @@ export const appRouter = {
|
||||
r.lastName,
|
||||
r.email,
|
||||
r.phone || "",
|
||||
r.artForm,
|
||||
r.wantsToPerform ? "Yes" : "No",
|
||||
r.artForm || "",
|
||||
r.experience || "",
|
||||
r.extraQuestions || "",
|
||||
r.createdAt.toISOString(),
|
||||
]);
|
||||
|
||||
|
||||
Binary file not shown.
24
packages/db/src/migrations/0000_mean_sunspot.sql
Normal file
24
packages/db/src/migrations/0000_mean_sunspot.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
ALTER TABLE `registration` ADD `wants_to_perform` integer DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `registration` ADD `extra_questions` text;--> statement-breakpoint
|
||||
-- art_form was NOT NULL, make it nullable by recreating the table
|
||||
-- SQLite does not support DROP NOT NULL directly, so we use the standard rename+recreate approach
|
||||
CREATE TABLE `registration_new` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`first_name` text NOT NULL,
|
||||
`last_name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`phone` text,
|
||||
`wants_to_perform` integer DEFAULT false NOT NULL,
|
||||
`art_form` text,
|
||||
`experience` text,
|
||||
`extra_questions` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);--> statement-breakpoint
|
||||
INSERT INTO `registration_new` (`id`, `first_name`, `last_name`, `email`, `phone`, `wants_to_perform`, `art_form`, `experience`, `extra_questions`, `created_at`)
|
||||
SELECT `id`, `first_name`, `last_name`, `email`, `phone`, `wants_to_perform`, `art_form`, `experience`, `extra_questions`, `created_at`
|
||||
FROM `registration`;--> statement-breakpoint
|
||||
DROP TABLE `registration`;--> statement-breakpoint
|
||||
ALTER TABLE `registration_new` RENAME TO `registration`;--> statement-breakpoint
|
||||
CREATE INDEX `registration_email_idx` ON `registration` (`email`);--> statement-breakpoint
|
||||
CREATE INDEX `registration_artForm_idx` ON `registration` (`art_form`);--> statement-breakpoint
|
||||
CREATE INDEX `registration_createdAt_idx` ON `registration` (`created_at`);
|
||||
558
packages/db/src/migrations/meta/0000_snapshot.json
Normal file
558
packages/db/src/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "dfeebea6-5c6c-4f28-9ecf-daf1f35f23ea",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"admin_request": {
|
||||
"name": "admin_request",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"requested_at": {
|
||||
"name": "requested_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"reviewed_at": {
|
||||
"name": "reviewed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reviewed_by": {
|
||||
"name": "reviewed_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"admin_request_user_id_unique": {
|
||||
"name": "admin_request_user_id_unique",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"admin_request_userId_idx": {
|
||||
"name": "admin_request_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"admin_request_status_idx": {
|
||||
"name": "admin_request_status_idx",
|
||||
"columns": [
|
||||
"status"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"columns": [
|
||||
"token"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"session_userId_idx": {
|
||||
"name": "session_userId_idx",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"verification": {
|
||||
"name": "verification",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"verification_identifier_idx": {
|
||||
"name": "verification_identifier_idx",
|
||||
"columns": [
|
||||
"identifier"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"registration": {
|
||||
"name": "registration",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"first_name": {
|
||||
"name": "first_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_name": {
|
||||
"name": "last_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"wants_to_perform": {
|
||||
"name": "wants_to_perform",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"art_form": {
|
||||
"name": "art_form",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"experience": {
|
||||
"name": "experience",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"extra_questions": {
|
||||
"name": "extra_questions",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"registration_email_idx": {
|
||||
"name": "registration_email_idx",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"registration_artForm_idx": {
|
||||
"name": "registration_artForm_idx",
|
||||
"columns": [
|
||||
"art_form"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"registration_createdAt_idx": {
|
||||
"name": "registration_createdAt_idx",
|
||||
"columns": [
|
||||
"created_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
packages/db/src/migrations/meta/_journal.json
Normal file
13
packages/db/src/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1772480169471,
|
||||
"tag": "0000_mean_sunspot",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,8 +9,12 @@ export const registration = sqliteTable(
|
||||
lastName: text("last_name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
phone: text("phone"),
|
||||
artForm: text("art_form").notNull(),
|
||||
wantsToPerform: integer("wants_to_perform", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
artForm: text("art_form"),
|
||||
experience: text("experience"),
|
||||
extraQuestions: text("extra_questions"),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
|
||||
Reference in New Issue
Block a user