Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/app/(landing)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import type { Metadata } from "next";
import { FAQ } from "@/components/landing/faq";
import { Features } from "@/components/landing/features";
import { Footer } from "@/components/landing/footer";
import dynamic from "next/dynamic";
import { Hero } from "@/components/landing/hero";
import { HowItWorks } from "@/components/landing/how-it-works";
import { Navbar } from "@/components/landing/navbar";
import { SocialProof } from "@/components/landing/social-proof";
import { ScrollParticles } from "@/components/landing/scroll-particles";

// Below-the-fold sections are code-split to keep the initial mobile bundle
// lean. SSR remains enabled so SEO and first paint are unaffected.
const Features = dynamic(() =>
import("@/components/landing/features").then((m) => m.Features),
);
const HowItWorks = dynamic(() =>
import("@/components/landing/how-it-works").then((m) => m.HowItWorks),
);
const SocialProof = dynamic(() =>
import("@/components/landing/social-proof").then((m) => m.SocialProof),
);
const FAQ = dynamic(() =>
import("@/components/landing/faq").then((m) => m.FAQ),
);
const Footer = dynamic(() =>
import("@/components/landing/footer").then((m) => m.Footer),
);

export const metadata: Metadata = {
title: "Devmetry — Track Your GitHub Metrics",
Expand All @@ -22,8 +37,9 @@ export const metadata: Metadata = {
export default function LandingPage() {
return (
<>
<ScrollParticles />
<Navbar />
<main className="relative bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,255,255,0.02)_2px,rgba(255,255,255,0.02)_4px)] bg-bg-base">
<main className="relative z-10 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,255,255,0.02)_2px,rgba(255,255,255,0.02)_4px)]">
<Hero />
<Features />
<HowItWorks />
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/faq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function FAQ() {
return (
<section ref={sectionRef} id="faq" className="relative py-24 lg:py-32">
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute bottom-0 left-1/4 h-[380px] w-[500px] rounded-full bg-accent-purple/[0.05] blur-3xl" />
<div className="absolute bottom-0 left-1/4 h-[380px] w-[500px] rounded-full bg-accent-purple/[0.05] blur-xl md:blur-3xl" />
</div>

<div className="relative mx-auto grid max-w-6xl grid-cols-1 gap-12 px-4 lg:grid-cols-[1fr_1.4fr] lg:gap-16 lg:px-8">
Expand Down
4 changes: 2 additions & 2 deletions src/components/landing/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export function Features() {
<section ref={sectionRef} id="features" className="relative py-24 lg:py-32">
{/* Ambient accent */}
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute top-0 left-1/2 h-[320px] w-[720px] -translate-x-1/2 rounded-full bg-accent-purple/[0.06] blur-3xl" />
<div className="absolute top-0 left-1/2 h-[320px] w-[720px] -translate-x-1/2 rounded-full bg-accent-purple/[0.06] blur-xl md:blur-3xl" />
</div>

<div className="relative mx-auto max-w-6xl px-4 lg:px-8">
Expand Down Expand Up @@ -269,7 +269,7 @@ export function Features() {
return (
<div
key={feature.title}
className={`feature-card group relative overflow-hidden rounded-xl border border-border bg-bg-surface/70 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 ${accent.border} ${
className={`feature-card group relative overflow-hidden rounded-xl border border-border bg-bg-surface/70 p-6 transition-all duration-500 hover:-translate-y-1 md:backdrop-blur-sm ${accent.border} ${
feature.span === "col-span-2"
? "md:col-span-2 lg:col-span-2"
: ""
Expand Down
60 changes: 33 additions & 27 deletions src/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,30 +138,36 @@ export function Hero() {
"-=1.2",
);

// Contribution grid — diagonal wave pulse (continuous)
gsap.to(".hero-cell", {
opacity: (_i, el) => Number((el as HTMLElement).dataset.base ?? 0.3),
duration: 1.4,
ease: "sine.inOut",
stagger: { each: 0.02, from: "start", grid: "auto" },
repeat: -1,
yoyo: true,
});
// Heavy continuous effects — desktop only. Mobile GPUs struggle
// with overlapping repeat loops + the other always-on animations.
const isDesktop = window.matchMedia("(min-width: 768px)").matches;

// Scanning line across the card
gsap.fromTo(
".hero-scan",
{ yPercent: -100, opacity: 0 },
{
yPercent: 200,
opacity: 1,
duration: 3.2,
delay: 1.4,
ease: "power2.inOut",
if (isDesktop) {
// Contribution grid — diagonal wave pulse (continuous)
gsap.to(".hero-cell", {
opacity: (_i, el) => Number((el as HTMLElement).dataset.base ?? 0.3),
duration: 1.4,
ease: "sine.inOut",
stagger: { each: 0.02, from: "start", grid: "auto" },
repeat: -1,
repeatDelay: 2.4,
},
);
yoyo: true,
});

// Scanning line across the card
gsap.fromTo(
".hero-scan",
{ yPercent: -100, opacity: 0 },
{
yPercent: 200,
opacity: 1,
duration: 3.2,
delay: 1.4,
ease: "power2.inOut",
repeat: -1,
repeatDelay: 2.4,
},
);
}

// Aurora blobs — slow drift
gsap.to(".hero-aurora-a", {
Expand Down Expand Up @@ -241,9 +247,9 @@ export function Hero() {
>
{/* Aurora background */}
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="hero-aurora-a absolute top-1/3 left-1/4 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-green/10 blur-3xl" />
<div className="hero-aurora-b absolute top-1/2 left-3/4 h-[460px] w-[460px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-purple/10 blur-3xl" />
<div className="absolute top-2/3 left-1/2 h-[380px] w-[380px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-cyan/[0.06] blur-3xl" />
<div className="hero-aurora-a absolute top-1/3 left-1/4 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-green/10 blur-xl md:blur-3xl" />
<div className="hero-aurora-b absolute top-1/2 left-3/4 h-[460px] w-[460px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-purple/10 blur-xl md:blur-3xl" />
<div className="absolute top-2/3 left-1/2 h-[380px] w-[380px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent-cyan/[0.06] blur-xl md:blur-3xl" />
{/* Subtle grid overlay */}
<div
className="absolute inset-0 opacity-[0.035]"
Expand All @@ -261,7 +267,7 @@ export function Hero() {
<div className="relative mx-auto grid w-full max-w-6xl grid-cols-1 items-center gap-12 px-4 lg:grid-cols-2 lg:gap-16 lg:px-8">
{/* Left column — text */}
<div className="flex flex-col gap-6">
<span className="hero-eyebrow inline-flex w-fit items-center gap-2 rounded-full border border-border bg-bg-surface/60 px-3 py-1 text-text-secondary text-xs backdrop-blur-sm">
<span className="hero-eyebrow inline-flex w-fit items-center gap-2 rounded-full border border-border bg-bg-surface/60 px-3 py-1 text-text-secondary text-xs md:backdrop-blur-sm">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-accent-green opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-accent-green" />
Expand Down Expand Up @@ -313,7 +319,7 @@ export function Hero() {

<div
ref={cardInnerRef}
className="hero-card relative overflow-hidden rounded-xl border border-border bg-bg-surface/90 shadow-2xl backdrop-blur-xl"
className="hero-card relative overflow-hidden rounded-xl border border-border bg-bg-surface/90 shadow-2xl md:backdrop-blur-xl"
style={{ transformStyle: "preserve-3d" }}
>
{/* Scanning line */}
Expand Down
4 changes: 2 additions & 2 deletions src/components/landing/how-it-works.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function HowItWorks() {
className="relative py-24 lg:py-32"
>
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute top-1/3 right-0 h-[400px] w-[500px] rounded-full bg-accent-cyan/[0.05] blur-3xl" />
<div className="absolute top-1/3 right-0 h-[400px] w-[500px] rounded-full bg-accent-cyan/[0.05] blur-xl md:blur-3xl" />
</div>

<div className="relative mx-auto max-w-5xl px-4 lg:px-8">
Expand Down Expand Up @@ -174,7 +174,7 @@ export function HowItWorks() {
</div>

{/* Terminal */}
<div className="overflow-hidden rounded-xl border border-border bg-bg-surface/90 shadow-lg backdrop-blur-sm lg:w-[340px] lg:shrink-0">
<div className="overflow-hidden rounded-xl border border-border bg-bg-surface/90 shadow-lg md:backdrop-blur-sm lg:w-[340px] lg:shrink-0">
<div className="flex items-center gap-1.5 border-border border-b bg-bg-elevated/80 px-3 py-2">
<span className="h-2 w-2 rounded-full bg-[#f85149]" />
<span className="h-2 w-2 rounded-full bg-[#d29922]" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/landing/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export function Navbar() {
className="fixed inset-0 z-40 flex flex-col items-center justify-center gap-8 bg-bg-base/95 pt-16 backdrop-blur-2xl"
>
{/* Ambient glow */}
<div className="pointer-events-none absolute top-1/3 left-1/2 h-[400px] w-[400px] -translate-x-1/2 rounded-full bg-accent-green/[0.08] blur-3xl" />
<div className="pointer-events-none absolute top-1/3 left-1/2 h-[400px] w-[400px] -translate-x-1/2 rounded-full bg-accent-green/[0.08] blur-xl md:blur-3xl" />

<div className="relative flex flex-col items-center gap-2">
<span className="mb-6 font-mono text-accent-green text-xs uppercase tracking-[0.3em]">
Expand Down
185 changes: 185 additions & 0 deletions src/components/landing/scroll-particles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// src/components/landing/scroll-particles.tsx
"use client";

import { useEffect, useRef } from "react";

/**
* Fixed-position canvas that renders floating code glyphs (0/1, {}, //, <>)
* drifting upward. Particle velocity reacts to scroll — the faster the user
* scrolls, the more the particles whoosh in the opposite direction.
*
* Pure canvas2d, respects prefers-reduced-motion, and pauses when tab hidden.
*/
const GLYPHS = [
"0",
"1",
"{",
"}",
"<",
">",
"/",
"*",
"=",
";",
"$",
"_",
"λ",
"#",
];
const COLORS = [
"rgba(0, 255, 65, ALPHA)", // accent-green
"rgba(124, 58, 237, ALPHA)", // accent-purple
"rgba(6, 182, 212, ALPHA)", // accent-cyan
];

type Particle = {
x: number;
y: number;
vy: number; // base upward velocity
size: number;
alpha: number;
glyph: string;
color: string;
rot: number;
vr: number;
};

export function ScrollParticles() {
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;

const reduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (reduced) return;

// Skip on touch-only devices and small viewports — RAF + fullscreen
// repaints + mix-blend are too expensive on mobile GPUs.
const isTouchOnly = window.matchMedia("(hover: none)").matches;
if (isTouchOnly || window.innerWidth < 900) return;

let dpr = Math.min(window.devicePixelRatio || 1, 2);
let width = 0;
let height = 0;
let particles: Particle[] = [];
let rafId = 0;
let lastScrollY = window.scrollY;
let scrollVelocity = 0;

function resize() {
if (!canvas || !ctx) return;
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
ctx.font = "600 12px var(--font-mono, monospace)";
}

function spawn(y?: number): Particle {
const alpha = 0.15 + Math.random() * 0.4;
const color = COLORS[Math.floor(Math.random() * COLORS.length)].replace(
"ALPHA",
String(alpha),
);
return {
x: Math.random() * width,
y: y ?? height + Math.random() * 200,
vy: 0.15 + Math.random() * 0.45,
size: 10 + Math.random() * 8,
alpha,
glyph: GLYPHS[Math.floor(Math.random() * GLYPHS.length)],
color,
rot: (Math.random() - 0.5) * 0.4,
vr: (Math.random() - 0.5) * 0.004,
};
}

function init() {
const count = Math.min(70, Math.floor((width * height) / 22000));
particles = Array.from({ length: count }, () => ({
...spawn(),
y: Math.random() * height,
}));
}

function onScroll() {
const currentY = window.scrollY;
scrollVelocity = currentY - lastScrollY;
lastScrollY = currentY;
}

function tick() {
if (!ctx) return;
ctx.clearRect(0, 0, width, height);

// Dampen scroll velocity each frame so it fades out when user stops
scrollVelocity *= 0.9;
// Cap influence so fast flicks don't explode the particles
const scrollPush = Math.max(-6, Math.min(6, scrollVelocity * 0.35));

for (const p of particles) {
p.y -= p.vy + scrollPush;
p.x += Math.sin((p.y + p.size) * 0.008) * 0.2;
p.rot += p.vr;

// Recycle
if (p.y < -20) {
Object.assign(p, spawn(height + 20));
} else if (p.y > height + 40) {
Object.assign(p, spawn(-20));
}

ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.font = `600 ${p.size}px var(--font-mono, ui-monospace, monospace)`;
ctx.fillText(p.glyph, 0, 0);
ctx.restore();
}

rafId = requestAnimationFrame(tick);
}

function onVisibility() {
if (document.hidden) {
cancelAnimationFrame(rafId);
} else {
rafId = requestAnimationFrame(tick);
}
}

resize();
init();
rafId = requestAnimationFrame(tick);
window.addEventListener("resize", () => {
resize();
init();
});
window.addEventListener("scroll", onScroll, { passive: true });
document.addEventListener("visibilitychange", onVisibility);

return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("scroll", onScroll);
document.removeEventListener("visibilitychange", onVisibility);
};
}, []);

return (
<canvas
ref={canvasRef}
aria-hidden
className="pointer-events-none fixed inset-0 z-0 opacity-70 mix-blend-screen"
/>
);
}
Loading
Loading