feat: wachtlijst flow + availableSpots naar 3

- Centrale workshop config (workshop.js) met availableSpots, totalSpots, isSoldOut
- isSoldOut vlag schakelt alle CTA's site-breed om naar wachtlijst
- Nieuwe pagina's: /wachtlijst-inschrijven (Tally kdyPJZ) en /wachtlijst-bedankt
- StickyBar, Hero, FinalCTA, Pricing tonen wachtlijsttekst bij isSoldOut: true
- Nog 3 plekken beschikbaar zichtbaar in StickyBar en FinalCTA
- Testimonial Jefta Bade foto toegevoegd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Frank Meeuwsen 2026-02-23 22:34:24 +01:00
parent 7d3f67bd2f
commit 9ea32169da
11 changed files with 264 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View file

@ -7,8 +7,11 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment'; import { PAYMENT_CONFIG } from '../config/payment';
import { WORKSHOP_CONFIG } from '../config/workshop';
function FinalCTA() { function FinalCTA() {
const { availableSpots, isSoldOut } = WORKSHOP_CONFIG;
return ( return (
<section className="py-16 lg:py-24 bg-coral-500 relative overflow-hidden"> <section className="py-16 lg:py-24 bg-coral-500 relative overflow-hidden">
{/* Decoratieve elementen */} {/* Decoratieve elementen */}
@ -42,7 +45,13 @@ function FinalCTA() {
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
<span>8 plaatsen</span> <span>Maximaal 8 deelnemers per workshop</span>
</div>
<div className="flex items-center gap-2 font-semibold text-white">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Er zijn nog {availableSpots} plekken beschikbaar</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -58,12 +67,12 @@ function FinalCTA() {
</div> </div>
</div> </div>
{/* CTA Button - naar inschrijfpagina */} {/* CTA Button - naar inschrijfpagina of wachtlijst */}
<Link <Link
to={PAYMENT_CONFIG.SIGNUP_URL} to={isSoldOut ? PAYMENT_CONFIG.WAITLIST_URL : PAYMENT_CONFIG.SIGNUP_URL}
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-coral-600 font-semibold text-lg rounded-xl shadow-lg hover:bg-coral-50 hover:shadow-xl active:bg-coral-100 transition-all duration-200" className="inline-flex items-center gap-2 px-8 py-4 bg-white text-coral-600 font-semibold text-lg rounded-xl shadow-lg hover:bg-coral-50 hover:shadow-xl active:bg-coral-100 transition-all duration-200"
> >
Doe mee op 6 maart {isSoldOut ? 'Zet me op de wachtlijst' : 'Doe mee op 6 maart'}
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg> </svg>

View file

@ -5,10 +5,13 @@
* Gebruikt coral accent kleuren en een decoratieve blob op de achtergrond. * Gebruikt coral accent kleuren en een decoratieve blob op de achtergrond.
*/ */
import { Link } from 'react-router-dom';
import { WORKSHOP_CONFIG } from '../config/workshop';
import { PAYMENT_CONFIG } from '../config/payment';
function Hero() { function Hero() {
// Aantal beschikbare plaatsen // Aantal beschikbare plaatsen - beheer via src/config/workshop.js
const totalSpots = 8; const { totalSpots, availableSpots, isSoldOut } = WORKSHOP_CONFIG;
const availableSpots = 7;
return ( return (
<section className="section relative overflow-hidden"> <section className="section relative overflow-hidden">
@ -34,7 +37,10 @@ function Hero() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-coral-500 opacity-75"></span> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-coral-500 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-coral-500"></span> <span className="relative inline-flex rounded-full h-2 w-2 bg-coral-500"></span>
</span> </span>
<span className="line-through opacity-60">{totalSpots}</span>{' '}{availableSpots} plekken - kleine groep, persoonlijke aandacht {isSoldOut
? 'Volgeboekt - wachtlijst beschikbaar'
: <><span className="line-through opacity-60">{totalSpots}</span>{' '}{availableSpots} plekken - kleine groep, persoonlijke aandacht</>
}
</span> </span>
</div> </div>
@ -108,15 +114,27 @@ function Hero() {
</div> </div>
{/* CTA Button */} {/* CTA Button */}
<a {isSoldOut ? (
href="#inschrijven" <Link
className="btn-primary inline-flex items-center gap-2" to={PAYMENT_CONFIG.WAITLIST_URL}
> className="btn-primary inline-flex items-center gap-2"
Schrijf je in >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> Zet me op de wachtlijst
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</a> </svg>
</Link>
) : (
<a
href="#inschrijven"
className="btn-primary inline-flex items-center gap-2"
>
Schrijf je in
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
)}
</div> </div>
{/* Rechter kolom: workshop foto */} {/* Rechter kolom: workshop foto */}

View file

@ -7,8 +7,10 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment'; import { PAYMENT_CONFIG } from '../config/payment';
import { WORKSHOP_CONFIG } from '../config/workshop';
function Pricing() { function Pricing() {
const { isSoldOut } = WORKSHOP_CONFIG;
// Wat is inbegrepen // Wat is inbegrepen
const included = [ const included = [
"Volledige workshop (9:00 - 14:00)", "Volledige workshop (9:00 - 14:00)",
@ -116,12 +118,12 @@ function Pricing() {
</div> </div>
</div> </div>
{/* CTA Button - naar inschrijfpagina */} {/* CTA Button - naar inschrijfpagina of wachtlijst */}
<Link <Link
to={PAYMENT_CONFIG.SIGNUP_URL} to={isSoldOut ? PAYMENT_CONFIG.WAITLIST_URL : PAYMENT_CONFIG.SIGNUP_URL}
className="btn-primary w-full text-center block" className="btn-primary w-full text-center block"
> >
Doe mee op 6 maart {isSoldOut ? 'Zet me op de wachtlijst' : 'Doe mee op 6 maart'}
</Link> </Link>
{/* Annuleringsbeleid als vertrouwenssignaal */} {/* Annuleringsbeleid als vertrouwenssignaal */}

View file

@ -9,8 +9,11 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment'; import { PAYMENT_CONFIG } from '../config/payment';
import { WORKSHOP_CONFIG } from '../config/workshop';
function StickyBar() { function StickyBar() {
const { availableSpots, isSoldOut } = WORKSHOP_CONFIG;
// State om te bepalen of de bar zichtbaar moet zijn // State om te bepalen of de bar zichtbaar moet zijn
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -57,6 +60,12 @@ function StickyBar() {
</svg> </svg>
Utrecht Utrecht
</span> </span>
<span className="flex items-center gap-1 text-coral-600 font-medium">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Nog {availableSpots} plekken beschikbaar
</span>
</div> </div>
</div> </div>
@ -66,10 +75,10 @@ function StickyBar() {
399 <span className="text-sm font-normal text-warm-500">excl. BTW</span> 399 <span className="text-sm font-normal text-warm-500">excl. BTW</span>
</span> </span>
<Link <Link
to={PAYMENT_CONFIG.SIGNUP_URL} to={isSoldOut ? PAYMENT_CONFIG.WAITLIST_URL : PAYMENT_CONFIG.SIGNUP_URL}
className="px-6 py-2 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors" className="px-6 py-2 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors"
> >
Inschrijven {isSoldOut ? 'Wachtlijst' : 'Inschrijven'}
</Link> </Link>
</div> </div>
</div> </div>
@ -88,14 +97,17 @@ function StickyBar() {
<div className="text-xs text-warm-500"> <div className="text-xs text-warm-500">
6 maart 2026 | Utrecht 6 maart 2026 | Utrecht
</div> </div>
<div className="text-xs text-coral-600 font-medium">
{isSoldOut ? 'Volgeboekt' : `Nog ${availableSpots} plekken beschikbaar`}
</div>
</div> </div>
{/* Right: CTA */} {/* Right: CTA */}
<Link <Link
to={PAYMENT_CONFIG.SIGNUP_URL} to={isSoldOut ? PAYMENT_CONFIG.WAITLIST_URL : PAYMENT_CONFIG.SIGNUP_URL}
className="flex-shrink-0 px-6 py-3 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors" className="flex-shrink-0 px-6 py-3 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors"
> >
Inschrijven {isSoldOut ? 'Wachtlijst' : 'Inschrijven'}
</Link> </Link>
</div> </div>
</div> </div>

View file

@ -27,7 +27,7 @@ function Testimonials() {
quote: "Onbeschrijfelijk inspirerend om dit mét een groep te doen. Dit zetje had ik net nodig.", quote: "Onbeschrijfelijk inspirerend om dit mét een groep te doen. Dit zetje had ik net nodig.",
name: "Jefta Bade", name: "Jefta Bade",
role: "Strategic Visualizer", role: "Strategic Visualizer",
avatar: "https://media.licdn.com/dms/image/v2/D4E03AQGgpwYSPHAihA/profile-displayphoto-crop_800_800/B4EZwo83s5GcAI-/0/1770213573365?e=1772064000&v=beta&t=i9hNyvY6KEfEJaGLOKJGsUgrfHMQBLtxn9L1pvhJh1s", avatar: `${import.meta.env.BASE_URL}20260211174-JeftaBade.jpg`,
url: "https://drawn.today/", url: "https://drawn.today/",
initials: "JB" initials: "JB"
}, },

View file

@ -4,8 +4,13 @@
* CTA-knoppen linken naar de inschrijfpagina met embedded Tally formulier. * CTA-knoppen linken naar de inschrijfpagina met embedded Tally formulier.
* Na het formulier redirect Tally naar de Mollie betaalpagina. * Na het formulier redirect Tally naar de Mollie betaalpagina.
* Test/live Mollie link wordt ingesteld in Tally's redirect URL. * Test/live Mollie link wordt ingesteld in Tally's redirect URL.
*
* WAITLIST_TALLY_ID: Tally formulier voor wachtlijst (alleen naam + e-mail, geen betaling).
* Tally URL: https://tally.so/r/kdyPJZ
*/ */
export const PAYMENT_CONFIG = { export const PAYMENT_CONFIG = {
SIGNUP_URL: '/inschrijven', SIGNUP_URL: '/inschrijven',
WAITLIST_URL: '/wachtlijst-inschrijven',
WAITLIST_TALLY_ID: 'kdyPJZ',
}; };

13
src/config/workshop.js Normal file
View file

@ -0,0 +1,13 @@
/**
* workshop.js - Workshop details configuratie
*
* Centrale plek voor beschikbaarheid en andere workshopdetails.
* Pas availableSpots hier aan als er een plek verkocht is.
* Zet isSoldOut op true als alle plekken weg zijn - activeert wachtlijstmodus op de hele site.
*/
export const WORKSHOP_CONFIG = {
totalSpots: 8,
availableSpots: 3,
isSoldOut: false,
};

View file

@ -7,6 +7,8 @@ import Privacy from './pages/Privacy.jsx'
import Terms from './pages/Terms.jsx' import Terms from './pages/Terms.jsx'
import ThankYou from './pages/ThankYou.jsx' import ThankYou from './pages/ThankYou.jsx'
import Signup from './pages/Signup.jsx' import Signup from './pages/Signup.jsx'
import WaitlistSignup from './pages/WaitlistSignup.jsx'
import WaitlistThankYou from './pages/WaitlistThankYou.jsx'
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
@ -17,6 +19,8 @@ createRoot(document.getElementById('root')).render(
<Route path="/voorwaarden" element={<Terms />} /> <Route path="/voorwaarden" element={<Terms />} />
<Route path="/inschrijven" element={<Signup />} /> <Route path="/inschrijven" element={<Signup />} />
<Route path="/bedankt" element={<ThankYou />} /> <Route path="/bedankt" element={<ThankYou />} />
<Route path="/wachtlijst-inschrijven" element={<WaitlistSignup />} />
<Route path="/wachtlijst-bedankt" element={<WaitlistThankYou />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</StrictMode>, </StrictMode>,

View file

@ -0,0 +1,94 @@
/**
* WaitlistSignup.jsx - Wachtlijst inschrijfpagina met embedded Tally formulier
*
* Bezoekers komen hier als de workshop volgeboekt is.
* Tally formulier verzamelt naam + e-mail, geen betaling.
* Tally ID: kdyPJZ (https://tally.so/r/kdyPJZ)
*/
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment';
function WaitlistSignup() {
// Laad het Tally embed script zodra de pagina mount
useEffect(() => {
const scriptUrl = 'https://tally.so/widgets/embed.js';
if (typeof window.Tally !== 'undefined') {
window.Tally.loadEmbeds();
return;
}
if (!document.querySelector(`script[src="${scriptUrl}"]`)) {
const script = document.createElement('script');
script.src = scriptUrl;
script.onload = () => {
if (typeof window.Tally !== 'undefined') {
window.Tally.loadEmbeds();
}
};
document.body.appendChild(script);
}
}, []);
return (
<div className="min-h-screen bg-warm-50">
{/* Header */}
<header className="bg-white border-b border-warm-200 py-6">
<div className="container-page">
<Link
to="/"
className="text-coral-500 hover:text-coral-600 transition-colors inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar workshop
</Link>
</div>
</header>
{/* Content */}
<main className="container-page py-12">
<div className="max-w-2xl mx-auto">
{/* Klokje icoon */}
<div className="mx-auto w-20 h-20 bg-coral-100 rounded-full flex items-center justify-center mb-8">
<svg className="w-10 h-10 text-coral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 className="heading-hero mb-4 text-center">Zet je op de wachtlijst</h1>
<p className="text-center text-warm-600 mb-4">
De workshop van 6 maart is volgeboekt. Zet je op de wachtlijst en we laten je weten als er een plek vrijkomt of als er een nieuwe editie gepland wordt.
</p>
<p className="text-center text-warm-500 text-sm mb-10">
Geen verplichtingen, geen betaling. Alleen je naam en e-mailadres.
</p>
{/* Embedded Tally wachtlijst formulier */}
<div className="card">
<iframe
data-tally-src={`https://tally.so/embed/${PAYMENT_CONFIG.WAITLIST_TALLY_ID}?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1`}
loading="lazy"
width="100%"
height="300"
frameBorder="0"
title="Wachtlijst Claude Code Workshop"
/>
</div>
</div>
</main>
{/* Footer */}
<footer className="bg-warm-900 text-warm-400 py-6">
<div className="container-page text-center text-sm">
&copy; {new Date().getFullYear()} Frank Meeuwsen. Alle rechten voorbehouden.
</div>
</footer>
</div>
);
}
export default WaitlistSignup;

View file

@ -0,0 +1,82 @@
/**
* WaitlistThankYou.jsx - Bevestigingspagina na wachtlijst inschrijving
*
* Bezoekers komen hier na het invullen van het wachtlijst formulier.
* Geen betaling - alleen bevestiging + uitleg wat er nu gebeurt.
*/
import { Link } from 'react-router-dom';
function WaitlistThankYou() {
return (
<div className="min-h-screen bg-warm-50">
{/* Header */}
<header className="bg-white border-b border-warm-200 py-6">
<div className="container-page">
<Link
to="/"
className="text-coral-500 hover:text-coral-600 transition-colors inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar homepage
</Link>
</div>
</header>
{/* Content */}
<main className="container-page py-12">
<div className="max-w-2xl mx-auto text-center">
{/* Klokje icoon */}
<div className="mx-auto w-20 h-20 bg-teal-100 rounded-full flex items-center justify-center mb-8">
<svg className="w-10 h-10 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="heading-hero mb-4">Je staat op de wachtlijst!</h1>
<p className="text-xl text-warm-600 mb-10">
Goed dat je je hebt aangemeld. We houden je op de hoogte.
</p>
{/* Wat er nu gebeurt */}
<div className="card text-left mb-10">
<h2 className="heading-3 mb-4">Wat gebeurt er nu?</h2>
<ol className="space-y-4 text-warm-600">
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">1</span>
<span>Als er een plek vrijkomt door uitval, nemen we als eerste contact met je op.</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">2</span>
<span>Als de wachtlijst groot genoeg is, plannen we een volgende editie. Je krijgt dan als eerste de uitnodiging.</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">3</span>
<span>Je ontvangt nooit spam. Alleen een bericht als er echt nieuws is.</span>
</li>
</ol>
</div>
{/* Contact */}
<p className="text-warm-500">
Vragen? Mail naar{' '}
<a href="mailto:frank@frankmeeuwsen.com" className="text-coral-500 hover:text-coral-600">
frank@frankmeeuwsen.com
</a>
</p>
</div>
</main>
{/* Footer */}
<footer className="bg-warm-900 text-warm-400 py-6">
<div className="container-page text-center text-sm">
&copy; {new Date().getFullYear()} Frank Meeuwsen. Alle rechten voorbehouden.
</div>
</footer>
</div>
);
}
export default WaitlistThankYou;