Files
greywall-landing-page/app/hackathons/page.tsx
2026-03-31 09:31:52 -04:00

549 lines
23 KiB
TypeScript

'use client'
import { useState, useEffect, useRef, Suspense, useCallback } 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, rootMargin: '0px 0px -40px 0px' })
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)
const eased = 1 - Math.pow(1 - p, 3) // ease-out cubic
setCount(Math.floor(eased * target))
if (p < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
}, [visible, target])
return <span ref={ref as React.RefObject<HTMLSpanElement>}>{count}{suffix}</span>
}
function useMouseSpotlight() {
const ref = useRef<HTMLDivElement>(null)
const onMove = useCallback((e: React.MouseEvent) => {
const el = ref.current
if (!el) return
const rect = el.getBoundingClientRect()
el.style.setProperty('--mouse-x', `${e.clientX - rect.left}px`)
el.style.setProperty('--mouse-y', `${e.clientY - rect.top}px`)
}, [])
return { ref, onMove }
}
/* ─── Noise overlay ─── */
function NoiseOverlay() {
return (
<svg className="fixed inset-0 w-full h-full pointer-events-none z-[100] opacity-[0.025]" aria-hidden>
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" stitchTiles="stitch" />
</filter>
<rect width="100%" height="100%" filter="url(#grain)" />
</svg>
)
}
/* ─── Dot grid (Linear-inspired) ─── */
function DotGrid({ cols = 16, rows = 5 }: { cols?: number; rows?: number }) {
return (
<div className="grid gap-6" style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
{Array.from({ length: cols * rows }, (_, i) => (
<div
key={i}
className="w-1 h-1 rounded-full bg-primary"
style={{
animation: 'dot-pulse 3.5s ease-in-out infinite',
animationDelay: `${(i % cols) * 80 + Math.floor(i / cols) * 120}ms`,
}}
/>
))}
</div>
)
}
/* ─── 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 ─── */
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 background */}
<div className="absolute inset-0 z-0 opacity-60">
<Suspense fallback={null}>
<ShieldScene />
</Suspense>
</div>
{/* Aurora gradient */}
<div className="absolute inset-0 z-[1] aurora-bg" />
{/* Dark overlay for readability */}
<div className="absolute inset-0 z-[1] bg-[radial-gradient(ellipse_at_center,rgba(22,22,20,0.3)_0%,rgba(22,22,20,0.75)_70%)]" />
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-background/50 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 text-shimmer">
Hack the Wall.
</h1>
<p className="text-xl sm:text-2xl text-muted-foreground font-serif mb-10 max-w-xl mx-auto">
Build on the AI agent security stack. Your best hacks get merged into Greywall.
</p>
<a
href="#tracks"
className="group 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 hover:shadow-[0_0_30px_rgba(217,94,42,0.3)]"
>
Explore tracks
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</a>
</div>
</section>
)
}
/* ─── Stats ─── */
function Stats() {
return (
<section className="py-14 px-4 sm:px-6 border-t border-b border-border/30 relative overflow-hidden">
{/* Dot grid background */}
<div className="absolute inset-0 flex items-center justify-center opacity-50 pointer-events-none">
<DotGrid cols={20} rows={3} />
</div>
<div className="relative 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>
)
}
/* ─── 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 relative overflow-hidden">
{/* Background glow behind terminal */}
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-[500px] h-[400px] bg-[radial-gradient(ellipse,rgba(217,94,42,0.06),transparent_70%)] blur-2xl pointer-events-none" />
<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&apos;s proxy captures every request, file access, and command your AI agent executes. This is what you&apos;ll be building on.
</p>
</div>
<div className="animate-float [animation-duration:6s]">
<LiveTerminal />
</div>
</div>
</section>
)
}
/* ─── Tracks ─── */
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)
const { ref: spotlightRef, onMove } = useMouseSpotlight()
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
ref={spotlightRef}
onMouseMove={onMove}
className={`card-spotlight group relative rounded-2xl border border-border/40 ${track.borderColor} bg-gradient-to-br ${track.color} transition-all duration-300 overflow-hidden hover:-translate-y-0.5 hover:shadow-[0_8px_40px_rgba(0,0,0,0.2)]`}
>
<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 group-hover:border-primary/20 transition-colors">
<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 hover:border-primary/20 hover:text-foreground transition-colors">
{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 relative">
{/* Subtle background glow */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[600px] bg-[radial-gradient(ellipse,rgba(217,94,42,0.04),transparent_70%)] pointer-events-none" />
<div className="relative 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-14 sm:py-20 px-4 sm:px-6 border-t border-border/30">
<div className="mx-auto max-w-5xl">
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight text-center mb-10">
How it works.
</h2>
<div className="relative grid grid-cols-2 sm:grid-cols-4 gap-8">
{/* Animated connector line */}
<div className="hidden sm:block absolute top-7 left-[12.5%] right-[12.5%] h-px overflow-hidden">
<div className={`h-full border-t border-dashed border-primary/30 transition-all duration-1000 ${visible ? 'w-full' : 'w-0'}`} style={{ transitionDelay: '300ms' }} />
</div>
{steps.map((step, i) => (
<div
key={step.title}
className={`relative text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-8 scale-95'}`}
style={{ transitionDelay: `${i * 150}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 hover:border-primary/30 hover:bg-card/80 transition-all group">
<step.icon className="h-6 w-6 text-primary group-hover:scale-110 transition-transform" />
</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-14 sm:py-20 px-4 sm:px-6 border-t border-border/30 relative overflow-hidden">
{/* Background dot grid */}
<div className="absolute inset-0 flex items-center justify-center opacity-30 pointer-events-none">
<DotGrid cols={24} rows={4} />
</div>
<div className="relative mx-auto max-w-5xl">
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight text-center mb-3">
More than a trophy.
</h2>
<p className="font-serif text-lg text-muted-foreground text-center mb-10">
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 backdrop-blur-sm hover:bg-card/50 hover:border-primary/20 hover:-translate-y-1 hover:shadow-[0_8px_30px_rgba(217,94,42,0.08)] transition-all duration-500 ${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-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-14 sm:py-20 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-3">
<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-3xl sm:text-4xl font-semibold tracking-tight mb-2">
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 group">
<h3 className="font-serif text-base sm:text-lg font-semibold group-hover:text-primary transition-colors">{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-16 sm:py-24 px-4 sm:px-6 border-t border-border/30 relative overflow-hidden">
{/* Aurora background */}
<div className="absolute inset-0 aurora-bg opacity-60" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(217,94,42,0.1)_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-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight mb-6 text-shimmer">
Build something that ships.
</h2>
<a href="mailto:hello@greyhaven.co" className="group 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 hover:shadow-[0_0_30px_rgba(217,94,42,0.3)]">
Get notified when registration opens
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</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 relative">
<NoiseOverlay />
<Nav />
<Hero />
<Stats />
<TerminalShowcase />
<Tracks />
<HowItWorks />
<Prizes />
<Location />
<FAQ />
<FinalCTA />
<Footer />
</main>
)
}