webpage start
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/web/public/assets/mic.png
Normal file
BIN
apps/web/public/assets/mic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
115
apps/web/src/components/homepage/ArtForms.tsx
Normal file
115
apps/web/src/components/homepage/ArtForms.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user