Compare commits
7 Commits
845624dfd3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
9502fec01f
|
|||
|
ee77644d83
|
|||
|
130cb9186d
|
|||
|
d3a4554b0d
|
|||
|
71b85d6874
|
|||
|
7b3d5461ef
|
|||
|
56942275b8
|
@@ -34,6 +34,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "catalog:",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"libsql": "catalog:",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
||||
@@ -8,11 +8,14 @@ import { Header } from "./Header";
|
||||
export function SiteHeader() {
|
||||
const { data: session } = authClient.useSession();
|
||||
const routerState = useRouterState();
|
||||
const isHomepage = routerState.location.pathname === "/";
|
||||
const pathname = routerState.location.pathname;
|
||||
const isHomepage = pathname === "/";
|
||||
const isKamp = pathname === "/kamp";
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isKamp) return;
|
||||
if (!isHomepage) {
|
||||
setIsVisible(true);
|
||||
return;
|
||||
@@ -25,7 +28,9 @@ export function SiteHeader() {
|
||||
handleScroll();
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [isHomepage]);
|
||||
}, [isHomepage, isKamp]);
|
||||
|
||||
if (isKamp) return null;
|
||||
|
||||
if (!session?.user) {
|
||||
return <Header isGuest isVisible={isVisible} isHomepage={isHomepage} />;
|
||||
|
||||
@@ -5,8 +5,169 @@ import { CountdownBanner } from "@/components/homepage/CountdownBanner";
|
||||
import { PerformerForm } from "@/components/registration/PerformerForm";
|
||||
import { TypeSelector } from "@/components/registration/TypeSelector";
|
||||
import { WatcherForm } from "@/components/registration/WatcherForm";
|
||||
import { POSTPONED } from "@/lib/opening";
|
||||
import { useRegistrationOpen } from "@/lib/useRegistrationOpen";
|
||||
import { orpc } from "@/utils/orpc";
|
||||
import { client, orpc } from "@/utils/orpc";
|
||||
|
||||
function PostponedBanner() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "loading" | "subscribed" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!email || status === "loading") return;
|
||||
setStatus("loading");
|
||||
try {
|
||||
await client.subscribeReminder({ email });
|
||||
setStatus("subscribed");
|
||||
} catch (err) {
|
||||
setErrorMessage(err instanceof Error ? err.message : "Er ging iets mis.");
|
||||
setStatus("error");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-8 py-4 text-center">
|
||||
{/* Postponed notice */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div
|
||||
className="inline-block px-4 py-1 font-['Intro',sans-serif] text-xs uppercase tracking-widest"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.12)",
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
}}
|
||||
>
|
||||
Aankondiging
|
||||
</div>
|
||||
<h2 className="font-['Intro',sans-serif] text-3xl text-white md:text-4xl">
|
||||
Dit evenement is uitgesteld
|
||||
</h2>
|
||||
<p className="max-w-lg text-base text-white/70 md:text-lg">
|
||||
De Open Mic Night van 24 april gaat helaas niet door op de geplande
|
||||
datum. We werken aan een nieuwe datum en laten je zo snel mogelijk
|
||||
weten wanneer het evenement plaatsvindt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hairline divider */}
|
||||
<div
|
||||
className="h-px w-24"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to right, transparent, rgba(255,255,255,0.2), transparent)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Waitlist signup */}
|
||||
<div className="flex w-full max-w-md flex-col items-center gap-4">
|
||||
<p className="font-['Intro',sans-serif] text-sm text-white/50 uppercase tracking-widest">
|
||||
Schrijf je in op de wachtlijst
|
||||
</p>
|
||||
<p className="text-sm text-white/60">
|
||||
Ontvang een e-mail zodra de nieuwe datum bekend is.
|
||||
</p>
|
||||
|
||||
{status === "subscribed" ? (
|
||||
<div
|
||||
className="flex flex-col items-center gap-2"
|
||||
style={{
|
||||
animation:
|
||||
"reminderCheck 0.4s cubic-bezier(0.34,1.56,0.64,1) both",
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes reminderCheck {
|
||||
from { opacity: 0; transform: scale(0.7); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.12)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
aria-label="Vinkje"
|
||||
role="img"
|
||||
>
|
||||
<path
|
||||
d="M2.5 7L5.5 10L11.5 4"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-['Intro',sans-serif] text-sm text-white/70 tracking-wide">
|
||||
Je staat op de wachtlijst! We houden je op de hoogte.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex w-full overflow-hidden"
|
||||
style={{
|
||||
borderRadius: "3px",
|
||||
border: "1px solid rgba(255,255,255,0.18)",
|
||||
background: "rgba(255,255,255,0.07)",
|
||||
backdropFilter: "blur(6px)",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="jouw@email.be"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={status === "loading"}
|
||||
className="min-w-0 flex-1 bg-transparent px-4 py-3 text-sm text-white outline-none placeholder:text-white/30 disabled:opacity-50"
|
||||
style={{ fontFamily: "'Intro', sans-serif", fontSize: "0.85rem" }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: "1px",
|
||||
background: "rgba(255,255,255,0.15)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading" || !email}
|
||||
className="shrink-0 px-5 py-3 font-['Intro',sans-serif] font-semibold text-white/90 text-xs tracking-wide transition-all disabled:opacity-40"
|
||||
style={{
|
||||
background: "transparent",
|
||||
cursor:
|
||||
status === "loading" || !email ? "not-allowed" : "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{status === "loading" ? "…" : "Inschrijven"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{status === "error" && (
|
||||
<p
|
||||
className="font-['Intro',sans-serif] text-xs"
|
||||
style={{ color: "rgba(255,140,140,0.8)" }}
|
||||
>
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fireConfetti() {
|
||||
const colors = ["#d82560", "#52979b", "#d09035", "#214e51", "#ffffff"];
|
||||
@@ -53,7 +214,7 @@ export default function EventRegistrationForm() {
|
||||
const { data: capacity } = useQuery(orpc.getWatcherCapacity.queryOptions());
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !confettiFired.current) {
|
||||
if (isOpen && !confettiFired.current && !POSTPONED) {
|
||||
confettiFired.current = true;
|
||||
fireConfetti();
|
||||
}
|
||||
@@ -63,6 +224,19 @@ export default function EventRegistrationForm() {
|
||||
null,
|
||||
);
|
||||
|
||||
if (POSTPONED) {
|
||||
return (
|
||||
<section
|
||||
id="registration"
|
||||
className="relative z-30 w-full bg-[#214e51]/96 px-6 py-16 md:px-12"
|
||||
>
|
||||
<div className="mx-auto w-full max-w-6xl">
|
||||
<PostponedBanner />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<section
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { POSTPONED } from "@/lib/opening";
|
||||
|
||||
export default function Hero() {
|
||||
const [micVisible, setMicVisible] = useState(false);
|
||||
@@ -82,11 +83,17 @@ export default function Hero() {
|
||||
|
||||
{/* Bottom Right - Dark Teal with date - above mic */}
|
||||
<div className="relative flex flex-1 items-center justify-end bg-[#214e51]">
|
||||
<p className="px-12 text-right font-['Intro',sans-serif] font-normal text-[clamp(2rem,5vw,6rem)] text-white uppercase leading-[1.1]">
|
||||
VRIJDAG 24
|
||||
<br />
|
||||
april
|
||||
</p>
|
||||
{POSTPONED ? (
|
||||
<p className="px-12 text-right font-['Intro',sans-serif] font-normal text-[clamp(1.5rem,4vw,4.5rem)] text-white uppercase leading-[1.1]">
|
||||
UITGESTELD
|
||||
</p>
|
||||
) : (
|
||||
<p className="px-12 text-right font-['Intro',sans-serif] font-normal text-[clamp(2rem,5vw,6rem)] text-white uppercase leading-[1.1]">
|
||||
VRIJDAG 24
|
||||
<br />
|
||||
april
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +104,7 @@ export default function Hero() {
|
||||
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 je nu!
|
||||
{POSTPONED ? "Schrijf je in op de wachtlijst" : "Registreer je nu!"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,11 +157,17 @@ export default function Hero() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col items-end justify-center bg-[#214e51] p-4">
|
||||
<p className="text-right font-['Intro',sans-serif] font-normal text-[8vw] text-white uppercase leading-tight">
|
||||
VRIJDAG
|
||||
<br />
|
||||
24 april
|
||||
</p>
|
||||
{POSTPONED ? (
|
||||
<p className="text-right font-['Intro',sans-serif] font-normal text-[6vw] text-white uppercase leading-tight">
|
||||
UITGESTELD
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-right font-['Intro',sans-serif] font-normal text-[8vw] text-white uppercase leading-tight">
|
||||
VRIJDAG
|
||||
<br />
|
||||
24 april
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +179,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 je nu!
|
||||
{POSTPONED ? "Schrijf je in op de wachtlijst" : "Registreer je nu!"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,3 +10,9 @@ export const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00");
|
||||
* Set to `true` to enforce the countdown until REGISTRATION_OPENS_AT.
|
||||
*/
|
||||
export const COUNTDOWN_ENABLED = true;
|
||||
|
||||
/**
|
||||
* Set to `true` when the event has been postponed.
|
||||
* Hides the registration form and shows a waitlist signup instead.
|
||||
*/
|
||||
export const POSTPONED = true;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Route as TermsRouteImport } from './routes/terms'
|
||||
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as KampRouteImport } from './routes/kamp'
|
||||
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
|
||||
import { Route as DrinkkaartRouteImport } from './routes/drinkkaart'
|
||||
import { Route as ContactRouteImport } from './routes/contact'
|
||||
@@ -46,6 +47,11 @@ const LoginRoute = LoginRouteImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const KampRoute = KampRouteImport.update({
|
||||
id: '/kamp',
|
||||
path: '/kamp',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
|
||||
id: '/forgot-password',
|
||||
path: '/forgot-password',
|
||||
@@ -113,6 +119,7 @@ export interface FileRoutesByFullPath {
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/kamp': typeof KampRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
@@ -131,6 +138,7 @@ export interface FileRoutesByTo {
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/kamp': typeof KampRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
@@ -150,6 +158,7 @@ export interface FileRoutesById {
|
||||
'/contact': typeof ContactRoute
|
||||
'/drinkkaart': typeof DrinkkaartRoute
|
||||
'/forgot-password': typeof ForgotPasswordRoute
|
||||
'/kamp': typeof KampRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/reset-password': typeof ResetPasswordRoute
|
||||
@@ -170,6 +179,7 @@ export interface FileRouteTypes {
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/kamp'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
@@ -188,6 +198,7 @@ export interface FileRouteTypes {
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/kamp'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
@@ -206,6 +217,7 @@ export interface FileRouteTypes {
|
||||
| '/contact'
|
||||
| '/drinkkaart'
|
||||
| '/forgot-password'
|
||||
| '/kamp'
|
||||
| '/login'
|
||||
| '/privacy'
|
||||
| '/reset-password'
|
||||
@@ -225,6 +237,7 @@ export interface RootRouteChildren {
|
||||
ContactRoute: typeof ContactRoute
|
||||
DrinkkaartRoute: typeof DrinkkaartRoute
|
||||
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
||||
KampRoute: typeof KampRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||
@@ -268,6 +281,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/kamp': {
|
||||
id: '/kamp'
|
||||
path: '/kamp'
|
||||
fullPath: '/kamp'
|
||||
preLoaderRoute: typeof KampRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/forgot-password': {
|
||||
id: '/forgot-password'
|
||||
path: '/forgot-password'
|
||||
@@ -361,6 +381,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
ContactRoute: ContactRoute,
|
||||
DrinkkaartRoute: DrinkkaartRoute,
|
||||
ForgotPasswordRoute: ForgotPasswordRoute,
|
||||
KampRoute: KampRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
ResetPasswordRoute: ResetPasswordRoute,
|
||||
|
||||
@@ -181,6 +181,7 @@ function AdminPage() {
|
||||
}),
|
||||
);
|
||||
const adminRequestsQuery = useQuery(orpc.getAdminRequests.queryOptions());
|
||||
const waitlistQuery = useQuery(orpc.getWaitlistSubscribers.queryOptions());
|
||||
|
||||
const exportMutation = useMutation({
|
||||
...orpc.exportRegistrations.mutationOptions(),
|
||||
@@ -553,6 +554,55 @@ function AdminPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Wachtlijst ── */}
|
||||
{waitlistQuery.data && waitlistQuery.data.length > 0 && (
|
||||
<div className="rounded-xl border border-white/8 bg-white/3 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<p className="font-mono text-[10px] text-white/40 uppercase tracking-widest">
|
||||
Wachtlijst
|
||||
</p>
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/60">
|
||||
{waitlistQuery.data.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-white/8 border-b text-left">
|
||||
<th className="pb-2 font-mono text-[10px] text-white/30 uppercase tracking-widest">
|
||||
E-mail
|
||||
</th>
|
||||
<th className="pb-2 font-mono text-[10px] text-white/30 uppercase tracking-widest">
|
||||
Ingeschreven op
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{waitlistQuery.data.map((row) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="border-white/5 border-b last:border-0"
|
||||
>
|
||||
<td className="py-2 pr-8 font-mono text-white/70 text-xs">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="py-2 font-mono text-white/40 text-xs">
|
||||
{new Date(row.createdAt).toLocaleString("nl-BE", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Filters + export row ── */}
|
||||
<div className="rounded-xl border border-white/8 bg-white/3 p-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-6">
|
||||
|
||||
@@ -1,24 +1,172 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import EventRegistrationForm from "@/components/homepage/EventRegistrationForm";
|
||||
import Footer from "@/components/homepage/Footer";
|
||||
import Hero from "@/components/homepage/Hero";
|
||||
import HoeInschrijven from "@/components/homepage/HoeInschrijven";
|
||||
import Info from "@/components/homepage/Info";
|
||||
|
||||
const KAMP_BANNER_KEY = "kk_kamp_banner_dismissed";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
function KampBanner({ onDismiss }: { onDismiss: () => void }) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!document.getElementById("kamp-banner-font")) {
|
||||
const el = document.createElement("link");
|
||||
el.id = "kamp-banner-font";
|
||||
el.rel = "stylesheet";
|
||||
el.href =
|
||||
"https://fonts.googleapis.com/css2?family=Special+Elite&display=swap";
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
const t = setTimeout(() => setVisible(true), 600);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setVisible(false);
|
||||
setTimeout(onDismiss, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<main className="relative">
|
||||
<Hero />
|
||||
<Info />
|
||||
<HoeInschrijven />
|
||||
<EventRegistrationForm />
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
<header
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: "1.25rem",
|
||||
left: "50%",
|
||||
transform: visible
|
||||
? "translateX(-50%) translateY(0)"
|
||||
: "translateX(-50%) translateY(120%)",
|
||||
transition: "transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||
zIndex: 50,
|
||||
width: "min(calc(100vw - 2rem), 480px)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "#ede4c8",
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E")`,
|
||||
border: "1px solid #12100e",
|
||||
outline: "3px solid rgba(18,16,14,0.1)",
|
||||
outlineOffset: "3px",
|
||||
borderRadius: 0,
|
||||
padding: "12px 14px 14px",
|
||||
boxShadow: "3px 5px 20px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{/* Top double rule */}
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
height: "2px",
|
||||
background: "#12100e",
|
||||
marginBottom: "3px",
|
||||
}}
|
||||
/>
|
||||
<div style={{ height: "1px", background: "rgba(18,16,14,0.35)" }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: "10px" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "8px",
|
||||
letterSpacing: "0.3em",
|
||||
textTransform: "uppercase",
|
||||
color: "rgba(18,16,14,0.45)",
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
✦ Aankondiging · Zomerkamp 2026 ✦
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontWeight: 700,
|
||||
fontSize: "14px",
|
||||
color: "#12100e",
|
||||
lineHeight: 1.35,
|
||||
marginBottom: "12px",
|
||||
}}
|
||||
>
|
||||
Inschrijvingen voor de zomerkampen zijn open!
|
||||
</p>
|
||||
<Link
|
||||
to="/kamp"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "0.22em",
|
||||
textTransform: "uppercase",
|
||||
textDecoration: "none",
|
||||
color: "#ede4c8",
|
||||
background: "#12100e",
|
||||
border: "1px solid rgba(18,16,14,0.4)",
|
||||
outline: "2px solid rgba(18,16,14,0.08)",
|
||||
outlineOffset: "3px",
|
||||
padding: "7px 12px 6px",
|
||||
}}
|
||||
>
|
||||
Schrijf je in →
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Sluit deze melding"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "rgba(18,16,14,0.3)",
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
flexShrink: 0,
|
||||
fontSize: "18px",
|
||||
lineHeight: 1,
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function HomePage() {
|
||||
const [showBanner, setShowBanner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem(KAMP_BANNER_KEY)) {
|
||||
setShowBanner(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismissBanner = () => {
|
||||
localStorage.setItem(KAMP_BANNER_KEY, "1");
|
||||
setShowBanner(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<main className="relative">
|
||||
<Hero />
|
||||
<Info />
|
||||
<HoeInschrijven />
|
||||
<EventRegistrationForm />
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
{showBanner && <KampBanner onDismiss={dismissBanner} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
473
apps/web/src/routes/kamp.tsx
Normal file
473
apps/web/src/routes/kamp.tsx
Normal file
@@ -0,0 +1,473 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const KAMP_URL = "https://ejv.be/jong/kampen/kunstenkamp/";
|
||||
|
||||
export const Route = createFileRoute("/kamp")({
|
||||
component: KampPage,
|
||||
});
|
||||
|
||||
const STYLES = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=UnifrakturMaguntia&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400;1,700&family=EB+Garamond:ital,wght@0,400;0,500;1,400;1,500&family=Special+Elite&display=swap');
|
||||
|
||||
@keyframes kampPressIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
body.kamp-page {
|
||||
background-color: #d6cdb0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.kamp-page ::selection {
|
||||
background: #12100e;
|
||||
color: #ede4c8;
|
||||
}
|
||||
|
||||
.kamp-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.kamp-scroll::-webkit-scrollbar-track {
|
||||
background: #c9c0a4;
|
||||
}
|
||||
.kamp-scroll::-webkit-scrollbar-thumb {
|
||||
background: #12100e;
|
||||
}
|
||||
`;
|
||||
|
||||
const PAPER = "#ede4c8";
|
||||
const INK = "#12100e";
|
||||
const INK_MID = "rgba(18,16,14,0.5)";
|
||||
const INK_GHOST = "rgba(18,16,14,0.2)";
|
||||
const RULE_W = "rgba(18,16,14,0.75)";
|
||||
|
||||
function TripleRule() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
|
||||
<div style={{ height: "3px", background: INK }} />
|
||||
<div style={{ height: "1px", background: RULE_W }} />
|
||||
<div style={{ height: "2px", background: INK }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DoubleRule() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
|
||||
<div style={{ height: "2px", background: RULE_W }} />
|
||||
<div style={{ height: "1px", background: RULE_W }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KampPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [ctaHovered, setCtaHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!document.getElementById("kamp-newspaper-styles")) {
|
||||
const el = document.createElement("style");
|
||||
el.id = "kamp-newspaper-styles";
|
||||
el.textContent = STYLES;
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
document.body.classList.add("kamp-page");
|
||||
const t = setTimeout(() => setMounted(true), 60);
|
||||
return () => {
|
||||
document.body.classList.remove("kamp-page");
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100dvh",
|
||||
background: "#d6cdb0",
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='400' height='400' filter='url(%23n)' opacity='0.12'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: "repeat",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "1.5rem 1rem",
|
||||
}}
|
||||
>
|
||||
{/* Newspaper page */}
|
||||
<article
|
||||
className="kamp-scroll"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "700px",
|
||||
height: "100%",
|
||||
background: PAPER,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E")`,
|
||||
border: `1px solid ${INK}`,
|
||||
boxShadow:
|
||||
"3px 5px 24px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.12)",
|
||||
opacity: mounted ? 1 : 0,
|
||||
animation: mounted ? "kampPressIn 0.9s ease both" : undefined,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{/* Outer decorative border inset */}
|
||||
<div
|
||||
style={{
|
||||
margin: "6px",
|
||||
border: `1px solid ${INK}`,
|
||||
}}
|
||||
>
|
||||
{/* Top flag strip */}
|
||||
<div
|
||||
style={{
|
||||
borderBottom: `1px solid ${INK}`,
|
||||
padding: "7px 20px 6px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "10px",
|
||||
letterSpacing: "0.18em",
|
||||
color: INK,
|
||||
textDecoration: "none",
|
||||
borderBottom: `1px solid ${INK}`,
|
||||
paddingBottom: "1px",
|
||||
}}
|
||||
>
|
||||
← Open Mic Night
|
||||
</Link>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "0.18em",
|
||||
color: INK_MID,
|
||||
}}
|
||||
>
|
||||
✦ · ✦ · ✦
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "0.22em",
|
||||
textTransform: "uppercase",
|
||||
color: INK_MID,
|
||||
}}
|
||||
>
|
||||
Anno 2026
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Masthead */}
|
||||
<div
|
||||
style={{
|
||||
padding: "20px 24px 16px",
|
||||
textAlign: "center",
|
||||
borderBottom: `1px solid ${INK}`,
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: "'UnifrakturMaguntia', cursive",
|
||||
fontSize: "clamp(2.4rem, 9vw, 4.5rem)",
|
||||
color: INK,
|
||||
margin: 0,
|
||||
lineHeight: 1,
|
||||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
De Kunstenkamp Gazet
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Publication bar */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "6px 20px 5px",
|
||||
borderBottom: `1px solid ${INK}`,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
"Vol. CCXXVI",
|
||||
"Zomerkamp · Juli 2026",
|
||||
"Prijs: uw aanwezigheid",
|
||||
].map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "10px",
|
||||
color: INK_MID,
|
||||
letterSpacing: "0.1em",
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div style={{ padding: "28px 28px 24px" }}>
|
||||
{/* Triple rule */}
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<TripleRule />
|
||||
</div>
|
||||
|
||||
{/* Main headline */}
|
||||
<div style={{ textAlign: "center", marginBottom: "16px" }}>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontWeight: 900,
|
||||
fontSize: "clamp(2.6rem, 9vw, 4.5rem)",
|
||||
color: INK,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.06em",
|
||||
lineHeight: 0.95,
|
||||
margin: "0 0 14px",
|
||||
}}
|
||||
>
|
||||
Wat Is Waar?
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontStyle: "italic",
|
||||
fontSize: "clamp(1rem, 2.5vw, 1.2rem)",
|
||||
color: INK_MID,
|
||||
margin: 0,
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
Een krant. Een tijdmachine. De waarheid doorheen de eeuwen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Triple rule */}
|
||||
<div style={{ marginBottom: "22px" }}>
|
||||
<TripleRule />
|
||||
</div>
|
||||
|
||||
{/* Editorial body copy */}
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'EB Garamond', serif",
|
||||
fontSize: "clamp(1.05rem, 2.2vw, 1.2rem)",
|
||||
color: INK,
|
||||
lineHeight: 1.75,
|
||||
margin: "0 0 10px",
|
||||
textAlign: "justify",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
In de zomer van 1826 — en opnieuw in 2026 — reizen onze
|
||||
verslaggevers terug naar de roerige redactiezalen van de
|
||||
negentiende eeuw. Waar de drukpers ronkt, de rookmachines tieren
|
||||
en elke kop een mening verbergt, stellen wij de vraag die door
|
||||
alle eeuwen galmt: <em>wat mogen wij geloven?</em>
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'EB Garamond', serif",
|
||||
fontStyle: "italic",
|
||||
fontSize: "clamp(0.95rem, 2vw, 1.08rem)",
|
||||
color: INK_MID,
|
||||
lineHeight: 1.7,
|
||||
margin: "0 0 24px",
|
||||
textAlign: "justify",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
Twee waarheden en een leugen. Vier bolhoeden en één typmachine.
|
||||
Bereid u voor op een week journalistiek, theater, dans en
|
||||
woordkunst — alles gehuld in inkt en papier-maché.
|
||||
</p>
|
||||
|
||||
{/* Ornamental divider */}
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontFamily: "'EB Garamond', serif",
|
||||
fontSize: "13px",
|
||||
color: INK_GHOST,
|
||||
margin: "0 0 24px",
|
||||
letterSpacing: "0.4em",
|
||||
}}
|
||||
>
|
||||
— ✦ · ✦ · ✦ —
|
||||
</p>
|
||||
|
||||
{/* Details grid */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
border: `1px solid ${INK}`,
|
||||
marginBottom: "28px",
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: "Primo", value: "20 – 25 Juli 2026" },
|
||||
{ label: "Secundo", value: "27 Juli – 1 Aug 2026" },
|
||||
{
|
||||
label: "Leeftijd",
|
||||
value: "9 – 18 jaar",
|
||||
sub: "* geboortejaar dat je 10 wordt",
|
||||
},
|
||||
{
|
||||
label: "Locatie",
|
||||
value: "Camp de Limauges",
|
||||
sub: "Ceroux-Mousty · België",
|
||||
},
|
||||
].map(({ label, value, sub }, i) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
padding: "16px 18px",
|
||||
borderRight: i % 2 === 0 ? `1px solid ${INK}` : undefined,
|
||||
borderTop: i >= 2 ? `1px solid ${INK}` : undefined,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "0.3em",
|
||||
textTransform: "uppercase",
|
||||
color: INK_MID,
|
||||
margin: "0 0 6px",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontWeight: 700,
|
||||
fontSize: "clamp(1rem, 2.5vw, 1.15rem)",
|
||||
color: INK,
|
||||
margin: 0,
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
{sub && (
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'EB Garamond', serif",
|
||||
fontStyle: "italic",
|
||||
fontSize: "13px",
|
||||
color: INK_MID,
|
||||
margin: "4px 0 0",
|
||||
}}
|
||||
>
|
||||
{sub}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Double rule */}
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<DoubleRule />
|
||||
</div>
|
||||
|
||||
{/* CTA — inverted ink block */}
|
||||
<div style={{ marginBottom: "4px" }}>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "9px",
|
||||
letterSpacing: "0.35em",
|
||||
textTransform: "uppercase",
|
||||
color: INK_MID,
|
||||
textAlign: "center",
|
||||
margin: "0 0 10px",
|
||||
}}
|
||||
>
|
||||
✦ Aankondiging ✦
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={KAMP_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onMouseEnter={() => setCtaHovered(true)}
|
||||
onMouseLeave={() => setCtaHovered(false)}
|
||||
style={{
|
||||
display: "block",
|
||||
background: ctaHovered ? "#2a2418" : INK,
|
||||
color: PAPER,
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
padding: "28px 32px 26px",
|
||||
outline: `2px solid ${INK}`,
|
||||
outlineOffset: "4px",
|
||||
transition: "background 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontWeight: 900,
|
||||
fontSize: "clamp(1.5rem, 5vw, 2.4rem)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
lineHeight: 1,
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
Schrijf U In
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontFamily: "'Special Elite', cursive",
|
||||
fontSize: "clamp(0.7rem, 2vw, 0.85rem)",
|
||||
letterSpacing: "0.25em",
|
||||
opacity: 0.65,
|
||||
}}
|
||||
>
|
||||
via ejv.be →
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: `1px solid ${INK}`,
|
||||
padding: "7px 20px 8px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'EB Garamond', serif",
|
||||
fontStyle: "italic",
|
||||
fontSize: "12px",
|
||||
color: INK_GHOST,
|
||||
margin: 0,
|
||||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
Kunst · Expressie · Avontuur · Waar is de waarheid?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
bun.lock
1
bun.lock
@@ -46,6 +46,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "catalog:",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"libsql": "catalog:",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
||||
@@ -8,3 +8,10 @@ export const OPENS = "maandag 16 maart 2026 om 19:00";
|
||||
|
||||
// Registration opens — used for reminder scheduling windows
|
||||
export const REGISTRATION_OPENS_AT = new Date("2026-03-16T19:00:00+01:00");
|
||||
|
||||
/**
|
||||
* Set to `true` when the event has been postponed.
|
||||
* Allows waitlist email subscriptions even after the original registration
|
||||
* open date has passed.
|
||||
*/
|
||||
export const POSTPONED = true;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
sum,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { REGISTRATION_OPENS_AT } from "../constants";
|
||||
import { POSTPONED, REGISTRATION_OPENS_AT } from "../constants";
|
||||
import {
|
||||
emailLog,
|
||||
sendCancellationEmail,
|
||||
@@ -902,6 +902,10 @@ export const appRouter = {
|
||||
return { success: true, message: "Admin toegang goedgekeurd" };
|
||||
}),
|
||||
|
||||
getWaitlistSubscribers: adminProcedure.handler(async () => {
|
||||
return db.select().from(reminder).orderBy(desc(reminder.createdAt));
|
||||
}),
|
||||
|
||||
rejectAdminRequest: adminProcedure
|
||||
.input(z.object({ requestId: z.string() }))
|
||||
.handler(async ({ input, context }) => {
|
||||
@@ -943,8 +947,9 @@ export const appRouter = {
|
||||
.handler(async ({ input, context }) => {
|
||||
const now = Date.now();
|
||||
|
||||
// Registration is already open — no point subscribing
|
||||
if (now >= REGISTRATION_OPENS_AT.getTime()) {
|
||||
// Registration is already open — no point subscribing, unless the event
|
||||
// is postponed (in which case we collect waitlist signups instead).
|
||||
if (now >= REGISTRATION_OPENS_AT.getTime() && !POSTPONED) {
|
||||
return { ok: false, reason: "already_open" as const };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user