feat: hackathon landing mvp
This commit is contained in:
469
app/hackathons/page.tsx
Normal file
469
app/hackathons/page.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, Suspense } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import {
|
||||
Activity,
|
||||
Shield,
|
||||
Code2,
|
||||
ChevronDown,
|
||||
ArrowRight,
|
||||
Users,
|
||||
Trophy,
|
||||
GitMerge,
|
||||
Star,
|
||||
Terminal,
|
||||
Clock,
|
||||
MapPin,
|
||||
Cpu,
|
||||
Boxes,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { LiveTerminal } from '@/components/hackathons/live-terminal'
|
||||
import { StreamViz, SecureViz, ExtendViz } from '@/components/hackathons/track-visuals'
|
||||
|
||||
const ShieldScene = dynamic(
|
||||
() => import('@/components/hackathons/shield-scene').then((m) => m.ShieldScene),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/* ─── Hooks ─── */
|
||||
|
||||
function useInView(threshold = 0.15) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVisible(true); obs.unobserve(el) } }, { threshold })
|
||||
obs.observe(el)
|
||||
return () => obs.disconnect()
|
||||
}, [threshold])
|
||||
return { ref, visible }
|
||||
}
|
||||
|
||||
function Counter({ target, suffix = '' }: { target: number; suffix?: string }) {
|
||||
const [count, setCount] = useState(0)
|
||||
const { ref, visible } = useInView(0.3)
|
||||
useEffect(() => {
|
||||
if (!visible) return
|
||||
let start = 0
|
||||
const step = (ts: number) => {
|
||||
if (!start) start = ts
|
||||
const p = Math.min((ts - start) / 1200, 1)
|
||||
setCount(Math.floor(p * target))
|
||||
if (p < 1) requestAnimationFrame(step)
|
||||
}
|
||||
requestAnimationFrame(step)
|
||||
}, [visible, target])
|
||||
return <span ref={ref as React.RefObject<HTMLSpanElement>}>{count}{suffix}</span>
|
||||
}
|
||||
|
||||
/* ─── Nav ─── */
|
||||
|
||||
function Nav() {
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-md">
|
||||
<div className="mx-auto max-w-5xl flex items-center justify-between px-6 h-14">
|
||||
<a href="/" className="flex items-center gap-2.5">
|
||||
<svg viewBox="0 0 32 32" fill="none" className="h-6 w-6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 2L4 7V15C4 22.18 9.11 28.79 16 30C22.89 28.79 28 22.18 28 15V7L16 2Z" fill="#D95E2A" />
|
||||
<path d="M16 6L8 9.5V15C8 20.05 11.42 24.68 16 26C20.58 24.68 24 20.05 24 15V9.5L16 6Z" fill="#161614" />
|
||||
<circle cx="16" cy="12" r="2" fill="#D95E2A" /><circle cx="12" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="20" cy="17" r="1.5" fill="#D95E2A" /><circle cx="16" cy="21" r="1.5" fill="#D95E2A" />
|
||||
<path d="M16 14V19.5M14 16L12.5 17M18 16L19.5 17" stroke="#D95E2A" strokeWidth="1" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="font-serif font-semibold text-lg tracking-tight">Greywall</span>
|
||||
</a>
|
||||
<div className="flex items-center gap-6">
|
||||
<a href="#tracks" className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block">Tracks</a>
|
||||
<a href="#faq" className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block">FAQ</a>
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Hero with 3D Shield ─── */
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="relative pt-28 sm:pt-40 pb-28 sm:pb-40 px-4 sm:px-6 overflow-hidden">
|
||||
{/* 3D Shield as background */}
|
||||
<div className="absolute inset-0 z-0 opacity-60">
|
||||
<Suspense fallback={null}>
|
||||
<ShieldScene />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Gradient overlays to keep text readable */}
|
||||
<div className="absolute inset-0 z-[1] bg-[radial-gradient(ellipse_at_center,rgba(22,22,20,0.4)_0%,rgba(22,22,20,0.8)_70%)]" />
|
||||
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-background/60 via-transparent to-background" />
|
||||
|
||||
<div className="relative z-[2] mx-auto max-w-4xl text-center">
|
||||
<h1 className="font-serif text-6xl sm:text-7xl md:text-8xl font-semibold tracking-tight leading-[1] mb-6">
|
||||
Hack the <em className="italic text-primary">Wall.</em>
|
||||
</h1>
|
||||
<p className="text-xl sm:text-2xl text-muted-foreground font-serif mb-8 max-w-xl mx-auto">
|
||||
Build on the AI agent security stack. Your best hacks get merged into Greywall.
|
||||
</p>
|
||||
<a
|
||||
href="#tracks"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-primary text-primary-foreground font-sans font-medium rounded-lg hover:bg-primary/90 transition-all glow-orange text-base"
|
||||
>
|
||||
Explore tracks
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Stats ─── */
|
||||
|
||||
function Stats() {
|
||||
return (
|
||||
<section className="py-12 px-4 sm:px-6 border-t border-b border-border/30">
|
||||
<div className="mx-auto max-w-3xl grid grid-cols-3 gap-8 text-center">
|
||||
{[
|
||||
{ value: 24, suffix: 'h', label: 'of hacking' },
|
||||
{ value: 3, suffix: '', label: 'open-ended tracks' },
|
||||
{ value: 100, suffix: '%', label: 'open source' },
|
||||
].map((stat) => (
|
||||
<div key={stat.label}>
|
||||
<div className="font-serif text-4xl sm:text-5xl font-semibold text-primary mb-1">
|
||||
<Counter target={stat.value} suffix={stat.suffix} />
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground font-sans">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Live Terminal Showcase ─── */
|
||||
|
||||
function TerminalShowcase() {
|
||||
const { ref, visible } = useInView(0.1)
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 sm:py-28 px-4 sm:px-6">
|
||||
<div className={`mx-auto max-w-5xl grid md:grid-cols-2 gap-10 items-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'}`}>
|
||||
<div>
|
||||
<p className="text-xs font-sans uppercase tracking-wider text-primary font-medium mb-4">The data stream</p>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Every agent action. In real time.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed">
|
||||
Greywall's proxy captures every request, file access, and command your AI agent executes. This is what you'll be building on.
|
||||
</p>
|
||||
</div>
|
||||
<LiveTerminal />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Tracks with visuals ─── */
|
||||
|
||||
const tracks = [
|
||||
{
|
||||
id: 'stream',
|
||||
icon: Activity,
|
||||
title: 'Build on the Stream',
|
||||
hook: 'One live data firehose. Make something cool with it.',
|
||||
color: 'from-orange-500/10 to-amber-500/5',
|
||||
borderColor: 'hover:border-orange-500/30',
|
||||
examples: ['Dashboards', 'Anomaly detection', 'Cost trackers', 'Behavior research', 'Bots', 'Art'],
|
||||
Visual: StreamViz,
|
||||
},
|
||||
{
|
||||
id: 'secure',
|
||||
icon: Shield,
|
||||
title: 'Secure Your Stack',
|
||||
hook: 'Bring your own project. Lock it down. Demo the tightest sandbox.',
|
||||
color: 'from-emerald-500/10 to-teal-500/5',
|
||||
borderColor: 'hover:border-emerald-500/30',
|
||||
examples: ['Policy templates', 'Threat models', 'Security writeups', 'Monitoring configs'],
|
||||
Visual: SecureViz,
|
||||
},
|
||||
{
|
||||
id: 'extend',
|
||||
icon: Code2,
|
||||
title: 'Extend Greywall',
|
||||
hook: 'Plugin, CLI tool, VS Code extension, web UI. If it\'s cool, it counts.',
|
||||
color: 'from-violet-500/10 to-purple-500/5',
|
||||
borderColor: 'hover:border-violet-500/30',
|
||||
examples: ['IDE plugins', 'NLP policies', 'Cost guardians', 'Grafana integrations', 'Wild ideas'],
|
||||
Visual: ExtendViz,
|
||||
},
|
||||
]
|
||||
|
||||
function TrackCard({ track, index }: { track: typeof tracks[0]; index: number }) {
|
||||
const { ref, visible } = useInView(0.1)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'}`}
|
||||
style={{ transitionDelay: `${index * 120}ms` }}
|
||||
>
|
||||
<div className={`group relative rounded-2xl border border-border/40 ${track.borderColor} bg-gradient-to-br ${track.color} transition-all duration-300 overflow-hidden`}>
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div className="flex">
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-8 sm:p-10 relative z-10">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-card/60 border border-border/30 backdrop-blur-sm">
|
||||
<track.icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground font-sans">
|
||||
<Clock className="h-3 w-3" />24h
|
||||
<Users className="h-3 w-3 ml-1" />Teams
|
||||
<MapPin className="h-3 w-3 ml-1" />Montreal
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-3">
|
||||
{track.title}
|
||||
</h3>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed mb-6 max-w-md">
|
||||
{track.hook}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{track.examples.map((ex) => (
|
||||
<span key={ex} className="px-3 py-1.5 text-xs font-sans font-medium rounded-full bg-card/60 border border-border/30 text-muted-foreground backdrop-blur-sm">
|
||||
{ex}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side visual */}
|
||||
<div className="hidden md:block w-[220px] shrink-0 relative overflow-hidden">
|
||||
<track.Visual />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Tracks() {
|
||||
return (
|
||||
<section id="tracks" className="py-24 sm:py-32 px-4 sm:px-6">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight mb-4">
|
||||
Pick your arena.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground">
|
||||
Three tracks. All open-ended. You bring the creativity.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{tracks.map((track, i) => (
|
||||
<TrackCard key={track.id} track={track} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── How It Works ─── */
|
||||
|
||||
const steps = [
|
||||
{ icon: Sparkles, title: 'Register', sub: 'All levels welcome' },
|
||||
{ icon: Terminal, title: 'Get set up', sub: 'We provide everything' },
|
||||
{ icon: Cpu, title: 'Hack for 24h', sub: 'Mentors on hand' },
|
||||
{ icon: Trophy, title: 'Demo & win', sub: 'Best hacks ship' },
|
||||
]
|
||||
|
||||
function HowItWorks() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
return (
|
||||
<section id="how-it-works" ref={ref} className="py-24 sm:py-32 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight text-center mb-16">
|
||||
How it works.
|
||||
</h2>
|
||||
|
||||
<div className="relative grid grid-cols-2 sm:grid-cols-4 gap-8">
|
||||
<div className="hidden sm:block absolute top-7 left-[12.5%] right-[12.5%] h-px border-t border-dashed border-border/50" />
|
||||
{steps.map((step, i) => (
|
||||
<div
|
||||
key={step.title}
|
||||
className={`relative text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
|
||||
style={{ transitionDelay: `${i * 100}ms` }}
|
||||
>
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-card/60 border border-border/40 mb-4 relative z-10 backdrop-blur-sm">
|
||||
<step.icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-serif text-lg font-semibold mb-1">{step.title}</h3>
|
||||
<p className="text-sm text-muted-foreground font-sans">{step.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Prizes ─── */
|
||||
|
||||
function Prizes() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
const items = [
|
||||
{ icon: GitMerge, title: 'Code gets merged', sub: 'Your hack becomes the product' },
|
||||
{ icon: Star, title: 'Contributor credit', sub: 'Name in the release notes' },
|
||||
{ icon: Boxes, title: 'Demo day invite', sub: 'Present to the community' },
|
||||
{ icon: Trophy, title: 'Community status', sub: 'Featured on greywall.io' },
|
||||
]
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-24 sm:py-32 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight text-center mb-4">
|
||||
More than a trophy.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground text-center mb-16">
|
||||
The best hacks ship.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6">
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className={`text-center p-6 rounded-2xl border border-border/30 bg-card/20 hover:bg-card/40 hover:border-primary/20 transition-all duration-500 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
|
||||
style={{ transitionDelay: `${i * 80}ms` }}
|
||||
>
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 border border-primary/15 mb-4">
|
||||
<item.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-serif text-base font-semibold mb-1">{item.title}</h3>
|
||||
<p className="text-xs text-muted-foreground font-sans">{item.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Location ─── */
|
||||
|
||||
function Location() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-24 sm:py-32 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className={`mx-auto max-w-5xl text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
<div className="inline-flex items-center gap-2 mb-4">
|
||||
<MapPin className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">Location</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight mb-4">
|
||||
Montreal.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground">
|
||||
Venue and dates announced soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── FAQ ─── */
|
||||
|
||||
const faqs = [
|
||||
{ q: 'Do I need security experience?', a: 'No. All experience levels welcome. If you can write code, you can participate.' },
|
||||
{ q: 'Do I need to know Greywall?', a: 'Nope. We provide setup support and Greywall maintainers are on hand throughout.' },
|
||||
{ q: 'Is it in-person only?', a: 'Yes. Hackathons are a social experience. Plus, you get direct access to maintainers all day.' },
|
||||
{ q: 'What happens to my code?', a: 'Your code is yours. Winning hacks get merged into Greywall with your full contributor credit, but only with your consent.' },
|
||||
{ q: 'What do I need to bring?', a: 'A laptop. We provide Greywall infrastructure, data streams, docs, food, and caffeine.' },
|
||||
]
|
||||
|
||||
function FAQ() {
|
||||
return (
|
||||
<section id="faq" className="py-24 sm:py-32 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight text-center mb-16">FAQ.</h2>
|
||||
<div>
|
||||
{faqs.map((faq) => <FAQItem key={faq.q} question={faq.q} answer={faq.a} />)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function FAQItem({ question, answer }: { question: string; answer: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div className="border-b border-border/30">
|
||||
<button onClick={() => setOpen(!open)} className="w-full flex items-center justify-between gap-4 py-5 text-left cursor-pointer">
|
||||
<h3 className="font-serif text-base sm:text-lg font-semibold">{question}</h3>
|
||||
<ChevronDown className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<div className={`grid transition-[grid-template-rows] duration-200 ${open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}>
|
||||
<div className="overflow-hidden">
|
||||
<p className="pb-5 text-muted-foreground font-serif text-base leading-relaxed">{answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Final CTA ─── */
|
||||
|
||||
function FinalCTA() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-28 sm:py-36 px-4 sm:px-6 border-t border-border/30 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(217,94,42,0.08)_0%,transparent_60%)]" />
|
||||
<div className={`relative mx-auto max-w-3xl text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight mb-8">
|
||||
Build something that <em className="italic text-primary">ships.</em>
|
||||
</h2>
|
||||
<a href="mailto:hello@greyhaven.co" className="inline-flex items-center gap-2 px-8 py-4 bg-primary text-primary-foreground font-sans font-medium rounded-lg hover:bg-primary/90 transition-all glow-orange text-base">
|
||||
Get notified when registration opens
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
<p className="text-xs text-muted-foreground/50 font-sans mt-8">
|
||||
Follow{' '}
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary/60 hover:text-primary/80 transition-colors">Greywall on GitHub</a>{' '}
|
||||
for updates.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Page ─── */
|
||||
|
||||
export default function HackathonsPage() {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Nav />
|
||||
<Hero />
|
||||
<Stats />
|
||||
<TerminalShowcase />
|
||||
<Tracks />
|
||||
<HowItWorks />
|
||||
<Prizes />
|
||||
<Location />
|
||||
<FAQ />
|
||||
<FinalCTA />
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user