webpage start

This commit is contained in:
2026-03-02 14:47:14 +01:00
parent 0856e154b9
commit 65d5ab71d7
59 changed files with 1889 additions and 1309 deletions

View File

@@ -1,24 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-lyra",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-lyra",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View File

@@ -1,62 +1,66 @@
{
"name": "web",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"serve": "vite preview",
"dev:bare": "vite dev"
},
"dependencies": {
"@base-ui/react": "^1.0.0",
"@kk/api": "workspace:*",
"@kk/auth": "workspace:*",
"@kk/env": "workspace:*",
"@libsql/client": "catalog:",
"@orpc/client": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "^1.12.2",
"@orpc/zod": "catalog:",
"@tailwindcss/vite": "^4.1.8",
"@tanstack/react-form": "^1.28.0",
"@tanstack/react-query": "^5.80.6",
"@tanstack/react-router": "^1.141.1",
"@tanstack/react-router-with-query": "^1.130.17",
"@tanstack/react-start": "^1.141.1",
"@tanstack/router-plugin": "^1.141.1",
"better-auth": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "catalog:",
"libsql": "catalog:",
"lucide-react": "^0.525.0",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"shadcn": "^3.6.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.3",
"tw-animate-css": "^1.2.5",
"vite-tsconfig-paths": "^5.1.4",
"zod": "catalog:"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.17.1",
"@kk/config": "workspace:*",
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-router-devtools": "^1.141.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.0.4",
"alchemy": "catalog:",
"jsdom": "^26.0.0",
"typescript": "catalog:",
"vite": "^7.0.2",
"web-vitals": "^5.0.3",
"wrangler": "^4.54.0"
}
"name": "web",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"serve": "vite preview",
"dev:bare": "vite dev"
},
"dependencies": {
"@base-ui/react": "^1.0.0",
"@kk/api": "workspace:*",
"@kk/auth": "workspace:*",
"@kk/env": "workspace:*",
"@libsql/client": "catalog:",
"@orpc/client": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "^1.12.2",
"@orpc/zod": "catalog:",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@tailwindcss/vite": "^4.1.8",
"@tanstack/react-form": "^1.28.0",
"@tanstack/react-query": "^5.80.6",
"@tanstack/react-router": "^1.141.1",
"@tanstack/react-router-with-query": "^1.130.17",
"@tanstack/react-start": "^1.141.1",
"@tanstack/router-plugin": "^1.141.1",
"better-auth": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "catalog:",
"libsql": "catalog:",
"lucide-react": "^0.525.0",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"shadcn": "^3.6.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.3",
"three": "^0.183.1",
"tw-animate-css": "^1.2.5",
"vite-tsconfig-paths": "^5.1.4",
"zod": "catalog:"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.17.1",
"@kk/config": "workspace:*",
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-router-devtools": "^1.141.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/three": "^0.183.1",
"@vitejs/plugin-react": "^5.0.4",
"alchemy": "catalog:",
"jsdom": "^26.0.0",
"typescript": "catalog:",
"vite": "^7.0.2",
"web-vitals": "^5.0.3",
"wrangler": "^4.54.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,115 @@
import { Camera, Drama, Mic2, Music, Palette, PenTool } from "lucide-react";
const artForms = [
{
icon: Music,
title: "Muziek",
description:
"Van akoestische singer-songwriter sets tot volledige band optredens. Ontdek je sound en deel je muziek met een warm publiek.",
trajectory: "Muziek Traject",
color: "#d82560",
},
{
icon: Drama,
title: "Theater",
description:
"Monologen, sketches, improvisatie of mime. Het podium is van jou — breng karakters tot leven en vertel verhalen die raken.",
trajectory: "Theater Traject",
color: "#52979b",
},
{
icon: Palette,
title: "Beeldende Kunst",
description:
"Live schilderen, illustraties maken, of mixed media performances. Toon je creatieve proces terwijl het publiek toekijkt.",
trajectory: "Beeldende Kunst Traject",
color: "#d09035",
},
{
icon: PenTool,
title: "Woordkunst",
description:
"Poëzie, spoken word, storytelling of rap. Laat je woorden dansen en raak het publiek met de kracht van taal.",
trajectory: "Woordkunst Traject",
color: "#214e51",
},
{
icon: Camera,
title: "Dans",
description:
"Contemporary, ballet, hiphop of freestyle. Beweging vertelt verhalen die woorden niet kunnen vangen.",
trajectory: "Dans Traject",
color: "#d82560",
},
{
icon: Mic2,
title: "Comedy",
description:
"Stand-up, improv of cabaret. Maak het publiek aan het lachen met je unieke kijk op de wereld.",
trajectory: "Comedy Traject",
color: "#52979b",
},
];
export default function ArtForms() {
return (
<section className="snap-section relative z-25 min-h-screen 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">
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) => {
const IconComponent = art.icon;
return (
<div
key={art.title}
className="group relative overflow-hidden bg-white p-8 transition-all hover:-translate-y-2 hover:shadow-xl"
>
{/* Color bar at top */}
<div
className="absolute top-0 left-0 h-1 w-full transition-all group-hover:h-2"
style={{ backgroundColor: art.color }}
/>
<div className="mb-4 flex items-center gap-4">
<div
className="flex h-14 w-14 items-center justify-center"
style={{ backgroundColor: art.color }}
>
<IconComponent className="h-7 w-7 text-white" />
</div>
<h3 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">
{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"
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>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -4,11 +4,12 @@ import { useState } from "react";
export default function EventRegistrationForm() {
const [formData, setFormData] = useState({
name: "",
firstName: "",
lastName: "",
email: "",
phone: "",
company: "",
dietaryRequirements: "",
artForm: "",
experience: "",
});
const handleSubmit = (e: React.FormEvent) => {
@@ -27,85 +28,156 @@ export default function EventRegistrationForm() {
return (
<section
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
id="registration"
className="snap-section relative z-30 min-h-screen w-full bg-[#214e51]/96 px-12 py-16"
>
<h2>Register for the Event</h2>
<div className="mx-auto flex h-full max-w-6xl flex-col">
<h2 className="mb-2 font-['Intro',sans-serif] text-4xl text-white">
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>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Full Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
placeholder="Enter your full name"
/>
</div>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<div className="flex flex-col gap-8">
{/* Row 1: First Name + Last Name */}
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label
htmlFor="firstName"
className="font-['Intro',sans-serif] text-2xl text-white"
>
Voornaam
</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Jouw voornaam"
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>
<div>
<label htmlFor="email">Email Address *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
placeholder="Enter your email"
/>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="lastName"
className="font-['Intro',sans-serif] text-2xl text-white"
>
Achternaam
</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Jouw achternaam"
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>
</div>
<div>
<label htmlFor="phone">Phone Number</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="Enter your phone number"
/>
</div>
{/* Row 2: Email + Phone */}
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="font-['Intro',sans-serif] text-2xl text-white"
>
E-mail
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="jouw@email.nl"
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>
<div>
<label htmlFor="company">Company / Organization</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder="Enter your company or organization"
/>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="phone"
className="font-['Intro',sans-serif] text-2xl text-white"
>
Telefoon
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
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"
/>
</div>
</div>
<div>
<label htmlFor="dietaryRequirements">
Dietary Requirements / Special Requests
</label>
<textarea
id="dietaryRequirements"
name="dietaryRequirements"
value={formData.dietaryRequirements}
onChange={handleChange}
rows={3}
placeholder="Any dietary restrictions or special requests?"
/>
</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."
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>
<button type="submit">Submit Registration</button>
{/* 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>
<input
type="text"
id="experience"
name="experience"
value={formData.experience}
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"
/>
</div>
</div>
</div>
<p>* Required fields</p>
</form>
{/* Submit button */}
<div className="mt-auto flex flex-col items-center gap-4 pt-12">
<button
type="submit"
className="bg-white px-12 py-4 font-['Intro',sans-serif] text-2xl text-[#214e51] transition-all hover:scale-105 hover:bg-gray-100"
>
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>
</div>
</form>
</div>
</section>
);
}

View File

@@ -1,13 +1,33 @@
export default function Footer() {
return (
<footer>
<div>
<div>© 2024 My App. All rights reserved.</div>
<footer className="snap-section 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
</h3>
<p className="mb-6 font-['Intro',sans-serif] text-white/80">
Waar creativiteit tot leven komt
</p>
<div>
<a href="/privacy">Privacy Policy</a>
<a href="/terms">Terms of Service</a>
<a href="/contact">Contact</a>
<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">
Privacy Policy
</a>
<span className="text-white/40">|</span>
<a href="/terms" className="transition-colors hover:text-white">
Terms of Service
</a>
<span className="text-white/40">|</span>
<a href="/contact" className="transition-colors hover:text-white">
Contact
</a>
</div>
<div className="mt-6 font-['Intro',sans-serif] text-white/50 text-xs">
© 2026 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>
</div>
</footer>

View File

@@ -1,21 +1,164 @@
import { useEffect, useState } from "react";
export default function Hero() {
const [micVisible, setMicVisible] = useState(false);
useEffect(() => {
// Trigger entrance animation after component mounts
const timer = setTimeout(() => {
setMicVisible(true);
}, 100);
return () => clearTimeout(timer);
}, []);
const scrollToRegistration = () => {
const registrationSection = document.getElementById("registration");
if (registrationSection) {
registrationSection.scrollIntoView({ behavior: "smooth" });
}
};
return (
<section
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<h1>Welcome to Our Event</h1>
<p>
Join us for an amazing experience. Register now to secure your spot.
</p>
<div>
<button type="button">Register Now</button>
<button type="button">Learn More</button>
<section className="snap-section relative h-screen 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 ${
micVisible
? "translate-x-0 translate-y-0 opacity-100"
: "translate-x-24 -translate-y-24 opacity-0"
}`}
style={{
right: "5%",
top: "0",
width: "38%",
}}
>
<img
src="/assets/mic.png"
alt="Vintage microphone"
className="h-full w-full object-contain drop-shadow-2xl"
/>
</div>
{/* Top row - 2:1 ratio using flex */}
<div className="relative flex h-[65%] min-h-0 flex-shrink-0 gap-[10px]">
{/* Top Left - Magenta with title - under mic */}
<div className="relative z-10 flex min-w-0 flex-[2] flex-col justify-center bg-[#d82560] px-[5%] py-[5%]">
<h1 className="font-['Intro',sans-serif] font-normal text-[clamp(3rem,7vw,6rem)] text-white uppercase leading-[1]">
OPEN MIC
<br />
NIGHT
</h1>
<p className="mt-4 font-['Intro',sans-serif] font-normal text-[clamp(1.25rem,2.5vw,2.625rem)] text-white/90">
Ongedesemd brood
</p>
</div>
{/* Top Right - Teal - under mic */}
<div className="relative z-10 min-w-0 flex-1 bg-[#52979b]" />
</div>
{/* Bottom row - equal width */}
<div className="relative z-30 flex min-h-0 flex-1 gap-[10px]">
{/* Bottom Left - Mustard - above mic */}
<div className="relative flex flex-1 items-center bg-[#d09035] px-[3%]">
<p className="font-['Intro',sans-serif] font-normal text-[clamp(1.25rem,2.5vw,2.625rem)] text-white leading-tight">
Een kunstenkamp
<br />
initiatief
</p>
</div>
{/* 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 18
<br />
april
</p>
</div>
</div>
{/* Desktop CTA Button - centered at bottom */}
<div className="absolute bottom-[10px] left-1/2 z-30 -translate-x-1/2">
<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!
</button>
</div>
</div>
{/* Mobile Responsive Layout */}
<div className="relative flex h-full w-full flex-col lg:hidden">
{/* Mobile: Single column stacked */}
<div className="flex flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
{/* Mobile Top - Magenta */}
<div className="relative z-10 flex flex-1 flex-col justify-center overflow-hidden bg-[#d82560] px-6 py-8">
<h1 className="font-['Intro',sans-serif] font-normal text-[15vw] text-white uppercase leading-[0.95]">
OPEN MIC
<br />
NIGHT
</h1>
<p className="mt-4 font-['Intro',sans-serif] font-normal text-[5vw] text-white/90">
Ongedesemd brood
</p>
{/* Mobile Microphone - positioned inside magenta section, clipped by overflow */}
<div
className={`pointer-events-none absolute z-10 transition-all duration-1000 ease-out ${
micVisible
? "translate-x-0 translate-y-0 opacity-100"
: "translate-x-12 -translate-y-12 opacity-0"
}`}
style={{
right: "-30%",
bottom: "-55%",
width: "70%",
height: "130%",
}}
>
<img
src="/assets/mic.png"
alt="Vintage microphone"
className="h-full w-full object-contain object-bottom drop-shadow-2xl"
/>
</div>
</div>
{/* Mobile Bottom Row - Mustard + Dark Teal */}
<div className="relative z-20 flex flex-1 gap-[10px]">
<div className="flex flex-1 items-center bg-[#d09035] px-4">
<p className="font-['Intro',sans-serif] font-normal text-[4vw] text-white leading-tight">
Een kunstenkamp
<br />
initiatief
</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 />
18 april
</p>
</div>
</div>
</div>
{/* Mobile CTA */}
<div className="px-[10px] pb-[10px]">
<button
onClick={scrollToRegistration}
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!
</button>
</div>
</div>
</section>
);

View File

@@ -1,47 +1,45 @@
const questions = [
{
question: "Wat is een open mic?",
answer:
"Een open mic is een podium waar iedereen zijn of haar talent kan tonen. Of je nu zingt, danst, een gedicht voordraagt of een instrument bespeelt — alle kunstvormen zijn welkom!",
},
{
question: "Wie mag er deelnemen?",
answer:
"Iedereen! Of je nu een doorgewinterde artiest bent of voor het eerst op een podium staat — de open mic night is er voor alle niveaus en ervaringen.",
},
{
question: "Hoelang mag mijn optreden duren?",
answer:
"Elke deelnemer krijgt 5-7 minuten podiumtijd. Zo houden we de avond dynamisch en krijgt iedereen een kans om te shinen.",
},
{
question: "Wat moet ik meenemen?",
answer:
"We zorgen voor geluidstechniek en een microfoon. Breng je eigen instrument mee als je dat nodig hebt. Vragen? Neem contact op via de registratie!",
},
];
export default function Info() {
return (
<section
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<div>
<div>
<h3>Event Details</h3>
<p>
Date: TBD
<br />
Location: TBD
<br />
Duration: Full Day
</p>
</div>
<div>
<h3>What to Expect</h3>
<p>
Keynote speakers
<br />
Networking opportunities
<br />
Workshops & sessions
</p>
</div>
<div>
<h3>Who Should Attend</h3>
<p>
Industry professionals
<br />
Students & educators
<br />
Anyone interested
</p>
</div>
<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>
</div>
))}
</div>
</section>
);

View File

@@ -1,9 +1,9 @@
import { Loader2 } from "lucide-react";
export default function Loader() {
return (
<div className="flex h-full items-center justify-center pt-8">
<Loader2 className="animate-spin" />
</div>
);
return (
<div className="flex h-full items-center justify-center pt-8">
<Loader2 className="animate-spin" />
</div>
);
}

View File

@@ -1,58 +1,57 @@
import type { VariantProps } from "class-variance-authority";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
destructive:
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-none",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
"group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-none border border-transparent bg-clip-padding font-medium text-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-none",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
...props
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -1,89 +1,103 @@
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Card({
className,
size = "default",
...props
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col",
className,
)}
{...props}
/>
);
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-none bg-card py-4 text-card-foreground text-xs/relaxed ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 has-data-[slot=card-footer]:pb-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
className,
)}
{...props}
/>
);
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("text-sm font-medium group-data-[size=sm]/card:text-sm", className)}
{...props}
/>
);
return (
<div
data-slot="card-title"
className={cn(
"font-medium text-sm group-data-[size=sm]/card:text-sm",
className,
)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-xs/relaxed", className)}
{...props}
/>
);
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-xs/relaxed", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
);
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"rounded-none border-t p-4 group-data-[size=sm]/card:p-3 flex items-center",
className,
)}
{...props}
/>
);
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-none border-t p-4 group-data-[size=sm]/card:p-3",
className,
)}
{...props}
/>
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -4,23 +4,23 @@ import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-none border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-1 aria-invalid:ring-1 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-none border border-input outline-none transition-colors after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 group-has-disabled/field:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -1,241 +1,262 @@
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { CheckIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-none shadow-md ring-1 duration-100 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
className,
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-none bg-popover text-popover-foreground shadow-md outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in data-closed:overflow-hidden",
className,
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuLabel({
className,
inset,
...props
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
inset?: boolean;
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn("text-muted-foreground px-2 py-2 text-xs data-[inset]:pl-8", className)}
{...props}
/>
);
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-2 text-muted-foreground text-xs data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
/>
);
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default select-none items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-[inset]:pl-8 data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-none shadow-lg ring-1 duration-100 w-auto",
className,
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
);
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-auto min-w-[96px] rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className,
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
className,
children,
checked,
...props
}: MenuPrimitive.CheckboxItem.Props) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
function DropdownMenuRadioItem({
className,
children,
...props
}: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
<span
className="pointer-events-none pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
}
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-muted-foreground text-xs tracking-widest group-focus/dropdown-menu-item:text-accent-foreground",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -1,20 +1,20 @@
import { Input as InputPrimitive } from "@base-ui/react/input";
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-none border bg-transparent px-2.5 py-1 text-xs transition-colors file:h-6 file:text-xs file:font-medium focus-visible:ring-1 aria-invalid:ring-1 md:text-xs file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -1,20 +1,21 @@
"use client";
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"gap-2 text-xs leading-none group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
className,
)}
{...props}
/>
);
return (
// biome-ignore lint/a11y/noLabelWithoutControl: This is a generic Label component - htmlFor will be provided when used
<label
data-slot="label"
className={cn(
"flex select-none items-center gap-2 text-xs leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -1,13 +1,16 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="skeleton"
className={cn("bg-muted rounded-none animate-pulse", className)}
{...props}
/>
);
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-none bg-muted", className)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -1,45 +1,44 @@
import type { ToasterProps } from "sonner";
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import type { ToasterProps } from "sonner";
import { Toaster as Sonner } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
);
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
);
};
export { Toaster };

View File

@@ -3,7 +3,7 @@ import { createServerFn } from "@tanstack/react-start";
import { authMiddleware } from "@/middleware/auth";
export const getUser = createServerFn({ method: "GET" })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return context.session;
});
.middleware([authMiddleware])
.handler(async ({ context }) => {
return context.session;
});

View File

@@ -1 +1,63 @@
@import "tailwindcss";
@import url("https://fonts.cdnfonts.com/css/intro");
/* Intro font family */
@font-face {
font-family: "Intro";
src: local("Intro"), local("Intro Regular");
font-weight: 400;
font-style: normal;
}
/* Smooth scrolling for the entire page */
html {
scroll-behavior: smooth;
}
/* Hide scrollbar but keep functionality */
html {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
/* Sticky scroll container with snap points */
.sticky-scroll-container {
height: 400vh; /* 4 sections x 100vh */
position: relative;
}
.sticky-section {
position: sticky;
top: 0;
height: 100vh;
width: 100%;
}
/* 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;
}

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}

View File

@@ -1,11 +1,13 @@
import { auth } from "@kk/auth";
import { createMiddleware } from "@tanstack/react-start";
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const session = await auth.api.getSession({
headers: request.headers,
});
return next({
context: { session },
});
});
export const authMiddleware = createMiddleware().server(
async ({ next, request }) => {
const session = await auth.api.getSession({
headers: request.headers,
});
return next({
context: { session },
});
},
);

View File

@@ -7,22 +7,22 @@ import { routeTree } from "./routeTree.gen";
import { orpc, queryClient } from "./utils/orpc";
export const getRouter = () => {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
context: { orpc, queryClient },
defaultPendingComponent: () => <Loader />,
defaultNotFoundComponent: () => <div>Not Found</div>,
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
});
return router;
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
context: { orpc, queryClient },
defaultPendingComponent: () => <Loader />,
defaultNotFoundComponent: () => <div>Not Found</div>,
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
});
return router;
};
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>;
}
interface Register {
router: ReturnType<typeof getRouter>;
}
}

View File

@@ -2,14 +2,14 @@ import { auth } from "@kk/auth";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/auth/$")({
server: {
handlers: {
GET: ({ request }) => {
return auth.handler(request);
},
POST: ({ request }) => {
return auth.handler(request);
},
},
},
server: {
handlers: {
GET: ({ request }) => {
return auth.handler(request);
},
POST: ({ request }) => {
return auth.handler(request);
},
},
},
});

View File

@@ -8,51 +8,51 @@ import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { createFileRoute } from "@tanstack/react-router";
const rpcHandler = new RPCHandler(appRouter, {
interceptors: [
onError((error) => {
console.error(error);
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
const apiHandler = new OpenAPIHandler(appRouter, {
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
plugins: [
new OpenAPIReferencePlugin({
schemaConverters: [new ZodToJsonSchemaConverter()],
}),
],
interceptors: [
onError((error) => {
console.error(error);
}),
],
});
async function handle({ request }: { request: Request }) {
const rpcResult = await rpcHandler.handle(request, {
prefix: "/api/rpc",
context: await createContext({ req: request }),
});
if (rpcResult.response) return rpcResult.response;
const rpcResult = await rpcHandler.handle(request, {
prefix: "/api/rpc",
context: await createContext({ req: request }),
});
if (rpcResult.response) return rpcResult.response;
const apiResult = await apiHandler.handle(request, {
prefix: "/api/rpc/api-reference",
context: await createContext({ req: request }),
});
if (apiResult.response) return apiResult.response;
const apiResult = await apiHandler.handle(request, {
prefix: "/api/rpc/api-reference",
context: await createContext({ req: request }),
});
if (apiResult.response) return apiResult.response;
return new Response("Not found", { status: 404 });
return new Response("Not found", { status: 404 });
}
export const Route = createFileRoute("/api/rpc/$")({
server: {
handlers: {
HEAD: handle,
GET: handle,
POST: handle,
PUT: handle,
PATCH: handle,
DELETE: handle,
},
},
server: {
handlers: {
HEAD: handle,
GET: handle,
POST: handle,
PUT: handle,
PATCH: handle,
DELETE: handle,
},
},
});

View File

@@ -1,5 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import ArtForms from "@/components/homepage/ArtForms";
import EventRegistrationForm from "@/components/homepage/EventRegistrationForm";
import Footer from "@/components/homepage/Footer";
import Hero from "@/components/homepage/Hero";
@@ -11,11 +12,14 @@ export const Route = createFileRoute("/")({
function HomePage() {
return (
<main>
<Hero />
<Info />
<EventRegistrationForm />
<Footer />
</main>
<div className="relative">
<main className="relative">
<Hero />
<Info />
<ArtForms />
<EventRegistrationForm />
<Footer />
</main>
</div>
);
}

View File

@@ -1,9 +1,8 @@
import type { RouterClient } from "@orpc/server";
import { createContext } from "@kk/api/context";
import { appRouter } from "@kk/api/routers/index";
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import type { RouterClient } from "@orpc/server";
import { createRouterClient } from "@orpc/server";
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
import { QueryCache, QueryClient } from "@tanstack/react-query";
@@ -12,39 +11,39 @@ import { getRequest } from "@tanstack/react-start/server";
import { toast } from "sonner";
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
toast.error(`Error: ${error.message}`, {
action: {
label: "retry",
onClick: query.invalidate,
},
});
},
}),
queryCache: new QueryCache({
onError: (error, query) => {
toast.error(`Error: ${error.message}`, {
action: {
label: "retry",
onClick: query.invalidate,
},
});
},
}),
});
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(appRouter, {
context: async () => {
return createContext({ req: getRequest() });
},
}),
)
.client((): RouterClient<typeof appRouter> => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
});
.server(() =>
createRouterClient(appRouter, {
context: async () => {
return createContext({ req: getRequest() });
},
}),
)
.client((): RouterClient<typeof appRouter> => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
});
return createORPCClient(link);
});
return createORPCClient(link);
});
export const client: RouterClient<typeof appRouter> = getORPCClient();

View File

@@ -1,28 +1,28 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -6,8 +6,14 @@ import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), tailwindcss(), tanstackStart(), viteReact(), alchemy()],
server: {
port: 3001,
},
plugins: [
tsconfigPaths(),
tailwindcss(),
tanstackStart(),
viteReact(),
alchemy(),
],
server: {
port: 3001,
},
});