-
- {item.question}
-
-
- {item.answer}
-
+
+ {/* Hero Section - Ongedesemd Brood */}
+
+ {/* Background pattern */}
+
+
+
+ {/* Decorative top element */}
+
+
+
+ {/* Flat matzo cracker - slightly rounded rect */}
+
+ {/* Perforation dots - characteristic matzo pattern */}
+ {[16, 28, 40, 52, 64].map((x) =>
+ [18, 28, 38].map((y) => (
+
+ )),
+ )}
+ {/* Score lines */}
+
+
+
+
- ))}
+
+ {/* Main Title */}
+
+ Ongedesemd
+
+ Brood?!
+
+
+
+ {/* Subtitle / Tagline */}
+
+ Kunst zonder franje · Puur vanuit het hart
+
+
+ {/* Decorative bottom element */}
+
+
+
+
+ {/* Content Section */}
+
+
+ {/* Ongedesemd Brood Explanation - Full Width Special Treatment */}
+
+
+
+
+ Waarom deze naam?
+
+
+
+
+
+ In de Bijbel staat ongedesemd brood (ook wel{" "}
+ matze genoemd) symbool voor
+ eenvoud en zuiverheid, zonder de 'ballast' van
+ desem.
+
+
+ Het werd gegeten tijdens Pesach als herinnering aan de
+ haastige uittocht uit Egypte, toen er geen tijd was om brood
+ te laten rijzen.
+
+
+
+
+ Bij ons staat het voor kunst zonder franje: geen ingewikkelde
+ regels of hoge drempels, gewoon authentieke expressie vanuit
+ het hart.
+
+
+
+ Kom vooral ook gewoon kijken!
+
+
+ Je hoeft absoluut niet te performen om te genieten van een
+ inspirerende avond vol muziek, poëzie en andere kunst.
+
+
+
+
+
+
+ {/* FAQ Section */}
+ {faqQuestions.map((item, idx) => (
+
+
+ {item.question}
+
+
{item.answer}
+
+ ))}
+
);
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 8b71e96..0d9dec0 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -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;
}
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 8ae1c5c..1f3a7ca 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -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,
}
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index c373109..5576074 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -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
()({
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()({
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()({
function RootDocument() {
return (
-
+
-
+
+ Ga naar hoofdinhoud
+
+
+
+
+
diff --git a/apps/web/src/routes/admin.tsx b/apps/web/src/routes/admin.tsx
index 4067122..012b08b 100644
--- a/apps/web/src/routes/admin.tsx
+++ b/apps/web/src/routes/admin.tsx
@@ -250,9 +250,7 @@ function AdminPage() {
className="flex items-center justify-between text-sm"
>
{item.artForm}
-
- {item.count}
-
+ {item.count}