feat: better design
This commit is contained in:
@@ -3,18 +3,12 @@
|
|||||||
import { useState, useEffect, useRef, Suspense, useCallback } from 'react'
|
import { useState, useEffect, useRef, Suspense, useCallback } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import {
|
import {
|
||||||
Shield,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Users,
|
Users,
|
||||||
Trophy,
|
|
||||||
|
|
||||||
Star,
|
Star,
|
||||||
Terminal,
|
|
||||||
Clock,
|
Clock,
|
||||||
MapPin,
|
MapPin,
|
||||||
Cpu,
|
|
||||||
Sparkles,
|
|
||||||
Eye,
|
Eye,
|
||||||
Target,
|
Target,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -46,23 +40,6 @@ function useInView(threshold = 0.15) {
|
|||||||
return { ref, visible }
|
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() {
|
function useMouseSpotlight() {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
@@ -89,24 +66,6 @@ function NoiseOverlay() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── 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 ─── */
|
/* ─── Nav ─── */
|
||||||
|
|
||||||
@@ -174,33 +133,6 @@ function Hero() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── 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: 48, suffix: 'h', label: 'of hacking' },
|
|
||||||
{ value: 5, suffix: '', label: '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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Notify Button ─── */
|
/* ─── Notify Button ─── */
|
||||||
|
|
||||||
@@ -637,17 +569,6 @@ function TrackCard({ track, index }: { track: typeof tracks[0]; index: number })
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 p-8 sm:p-10 relative z-10">
|
<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" />48h
|
|
||||||
<Users className="h-3 w-3 ml-1" />Teams of 2–3
|
|
||||||
<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">
|
<h3 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-3">
|
||||||
{track.title}
|
{track.title}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -708,95 +629,6 @@ function Tracks() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── How It Works ─── */
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{ icon: Sparkles, title: 'Form a team', sub: 'Groups of 2–3' },
|
|
||||||
{ icon: Terminal, title: 'Get set up', sub: 'Docs & infra provided' },
|
|
||||||
{ icon: Cpu, title: 'Hack for 48h', sub: 'Collect achievements' },
|
|
||||||
{ icon: Trophy, title: 'Present & win', sub: 'Top teams demo' },
|
|
||||||
]
|
|
||||||
|
|
||||||
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-3xl">
|
|
||||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight text-center mb-12">
|
|
||||||
How it works.
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="relative grid grid-cols-4 gap-6">
|
|
||||||
{/* 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 bg-gradient-to-r from-primary/40 via-primary/20 to-primary/40 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-gradient-to-br from-primary/15 to-primary/5 border border-primary/20 mb-4 relative z-10 hover:border-primary/40 transition-all group">
|
|
||||||
<step.icon className="h-6 w-6 text-primary group-hover:scale-110 transition-transform" />
|
|
||||||
</div>
|
|
||||||
<h3 className="font-serif text-base font-semibold mb-1">{step.title}</h3>
|
|
||||||
<p className="text-xs text-muted-foreground font-sans">{step.sub}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Prizes ─── */
|
|
||||||
|
|
||||||
function Prizes() {
|
|
||||||
const { ref, visible } = useInView()
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ icon: Crown, title: 'Hall of Fame', sub: 'Featured on greywall.io permanently' },
|
|
||||||
{ icon: Star, title: 'Contributor credit', sub: 'Added as a contributor in the GitHub README' },
|
|
||||||
]
|
|
||||||
|
|
||||||
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 pointer-events-none opacity-40"
|
|
||||||
style={{
|
|
||||||
backgroundImage: 'radial-gradient(circle, rgba(217,94,42,0.5) 1px, transparent 1px)',
|
|
||||||
backgroundSize: '32px 32px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative mx-auto max-w-3xl">
|
|
||||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight text-center mb-10">
|
|
||||||
What you win.
|
|
||||||
</h2>
|
|
||||||
<div className="flex justify-center 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 ─── */
|
/* ─── Location ─── */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user