From f43b244499dbe330b8e6af0c58e220a9c39aec81 Mon Sep 17 00:00:00 2001 From: riddhima Date: Tue, 9 Jun 2026 21:59:56 +0530 Subject: [PATCH 1/7] feat(onboarding): add role selection page --- src/app/onboarding/page.tsx | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/app/onboarding/page.tsx diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx new file mode 100644 index 0000000..87d73f8 --- /dev/null +++ b/src/app/onboarding/page.tsx @@ -0,0 +1,85 @@ +export default function OnboardingPage() { + return ( +
+
+
+ WELCOME TO MERGESHIP +
+ +

How are you joining?

+ +

Pick your path to streamline your open-source journey.

+ +
+ {/* Contributor Card */} +
+
+ + FIRST PR + + ▸_ + + +50 XP + +
+ +

FOR CONTRIBUTORS

+ +

I want to contribute

+ +

+ Get a structured path into open source. Find mentored issues, track your impact, and + build your profile. +

+ +
    +
  • ✓ Match with mentored issues
  • +
  • ✓ Step-by-step PR guidance
  • +
  • ✓ Build a verified portfolio
  • +
+ + +
+ + {/* Maintainer Card */} +
+
+ + AI FLAGGED + + ▦+ + + -74% NOISE + +
+ +

FOR MAINTAINERS

+ +

I maintain a project

+ +

+ Connect your org and get a smart PR queue. Reduce noise, onboard contributors faster, + and ship clean code. +

+ +
    +
  • ✓ Automated PR triaging
  • +
  • ✓ AI-assisted code reviews
  • +
  • ✓ Contributor analytics
  • +
+ + +
+
+ +

+ Not sure? Start as a contributor. +

+
+
+ ); +} From 8d706e4435fa7935df6c599e984d5095078b7b10 Mon Sep 17 00:00:00 2001 From: riddhima Date: Thu, 11 Jun 2026 00:47:20 +0530 Subject: [PATCH 2/7] fix(onboarding): connect role selection flow --- src/app/onboarding/page.tsx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 87d73f8..aa2b02d 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -1,3 +1,5 @@ +import Link from 'next/link'; + export default function OnboardingPage() { return (
@@ -11,7 +13,6 @@ export default function OnboardingPage() {

Pick your path to streamline your open-source journey.

- {/* Contributor Card */}
@@ -24,7 +25,6 @@ export default function OnboardingPage() {

FOR CONTRIBUTORS

-

I want to contribute

@@ -38,12 +38,14 @@ export default function OnboardingPage() {

  • ✓ Build a verified portfolio
  • - +
    - {/* Maintainer Card */}
    @@ -56,7 +58,6 @@ export default function OnboardingPage() {

    FOR MAINTAINERS

    -

    I maintain a project

    @@ -70,14 +71,20 @@ export default function OnboardingPage() {

  • ✓ Contributor analytics
  • - +

    - Not sure? Start as a contributor. + Not sure?{' '} + + Start as a contributor. +

    From 96d77657c38a791b9b110c397b026d68b24ecf63 Mon Sep 17 00:00:00 2001 From: riddhima Date: Thu, 11 Jun 2026 11:35:53 +0530 Subject: [PATCH 3/7] fix(onboarding): route landing CTAs through onboarding --- src/components/landing/LandingPage.tsx | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index ba97eac..37328c9 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -298,9 +298,9 @@ function NavAuth() { } return ( - + ); } @@ -410,18 +410,7 @@ function Hero() { MergeShip changes that — level by level, PR by PR. - { - const isLoggedIn = false; - if (!isLoggedIn) { - e.preventDefault(); - alert("Please sign in first to continue."); - window.location.href = "/signin"; - } - }} - > + Start Contributing → see how it works → @@ -453,7 +442,7 @@ function Hero() { and lets peer-verified PRs reach you pre-checked. - Connect Your Org → + Connect Your Org → see the dashboard → @@ -837,7 +826,7 @@ function CtaSplit() {

    Your first real contribution starts here.

    Connect your org. Review with confidence.
    Date: Thu, 11 Jun 2026 12:16:41 +0530 Subject: [PATCH 4/7] fix(onboarding): route landing flow through onboarding --- src/components/landing/LandingPage.tsx | 1003 ++++++++++++++++-------- 1 file changed, 673 insertions(+), 330 deletions(-) diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index 4f81a64..43125ac 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -1,121 +1,233 @@ +/* eslint-disable */ +// @ts-nocheck — partner's landing page; backend rebuild keeps it untouched except for auth swap 'use client'; import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { motion, useScroll, animate, useReducedMotion } from 'framer-motion'; +import dynamic from 'next/dynamic'; import Link from 'next/link'; -import { - AlertTriangle, - Bot, - Zap, - Lock, - Clock, - Menu, - X, - ArrowRight, - ChevronRight, -} from 'lucide-react'; import { getBrowserSupabase } from '@/lib/supabase/browser'; import '@/app/landing.css'; type NavUser = { name: string | null; email: string | null }; -function isLocalSupabase(): boolean { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; - return url.includes('127.0.0.1') || url.includes('localhost'); -} - -/* ── Animated counter ──────────────────────────────────────────────────── */ -function StatNumber({ value, duration = 2 }: { value: string; duration?: number }) { - const ref = useRef(null); - const [inView, setInView] = useState(false); +const HeroScene = dynamic(() => import('./HeroScene'), { ssr: false }); - const match = useMemo(() => { - const m = /^([^0-9.]*)([0-9.]+)(.*)$/.exec(value); - if (!m) return null; - return { - prefix: m[1] ?? '', - num: parseFloat(m[2] ?? '0'), - suffix: m[3] ?? '', - }; - }, [value]); +// ─── Shared hook ──────────────────────────────────────────────────────────── +function useInView(ref: React.RefObject, opts: { once?: boolean; margin?: string; fallbackMs?: number } = {}) { + const { once = true, margin = '0px', fallbackMs = 1800 } = opts; + const [inView, setInView] = useState(false); useEffect(() => { if (!ref.current) return; + let done = false; + const reveal = () => { if (!done) { done = true; setInView(true); } }; const io = new IntersectionObserver( (entries) => { - const e = entries[0]; - if (e && e.isIntersecting) { - setInView(true); - io.disconnect(); + for (const e of entries) { + if (e.isIntersecting) { reveal(); if (once) io.disconnect(); } + else if (!once) setInView(false); } }, - { threshold: 0.15 }, + { rootMargin: margin, threshold: 0.05 } ); io.observe(ref.current); - return () => io.disconnect(); + const t = setTimeout(reveal, fallbackMs); + return () => { io.disconnect(); clearTimeout(t); }; }, []); + return inView; +} - const num = match ? match.num : 0; - const start = useMemo(() => (Number.isFinite(num) && num > 0 ? num * 0.3 : 0), [num]); +// ─── FadeUp ────────────────────────────────────────────────────────────────── - const fmt = useCallback( - (v: number) => { - if (num >= 1000) return Math.round(v).toLocaleString(); - if (num % 1 !== 0) return v.toFixed(1); - return Math.round(v).toString(); - }, - [num], +function FadeUp({ + delay = 0, y = 16, duration = 0.6, children, className = '', + style = {}, as: As = 'div', +}: { + delay?: number; y?: number; duration?: number; children?: React.ReactNode; + className?: string; style?: React.CSSProperties; as?: React.ElementType; +}) { + return ( + + {children} + ); +} + +// ─── SplitText ─────────────────────────────────────────────────────────────── + +function SplitText({ text, delay = 0 }: { text: string; delay?: number }) { + const words = text.split(' '); + let charIndex = 0; + const elements: React.ReactNode[] = []; + words.forEach((word, wi) => { + const isItalic = word.startsWith('*') && word.endsWith('*'); + const clean = isItalic ? word.slice(1, -1) : word; + elements.push( + + {clean.split('').map((c, ci) => { + const idx = charIndex++; + return ( + + {c} + + ); + })} + + ); + if (wi < words.length - 1) elements.push( ); + }); + return {elements}; +} + +// ─── StatNumber ────────────────────────────────────────────────────────────── + +function StatNumber({ value, duration = 2 }: { value: string; duration?: number }) { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); + const prefersReducedMotion = useReducedMotion(); + const isPercent = value.includes('%'); + const isNeg = value.startsWith('−'); + const isK = value.toLowerCase().endsWith('k'); + const num = parseFloat(value.replace(/[^0-9.]/g, '')); + const suffix = isPercent ? '%' : isK ? 'k' : ''; + const startValue = useMemo(() => { + if (!Number.isFinite(num) || num <= 0) return 0; + let hash = 0; + for (let i = 0; i < value.length; i++) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + const r = (hash % 1000) / 1000; + const minRatio = 0.25; + const maxRatio = 0.6; + const ratio = minRatio + r * (maxRatio - minRatio); + return num * ratio; + }, [num, value]); + + const fmt = useCallback((v: number) => { + if (num >= 1000) return Math.round(v).toLocaleString(); + if (isK) return v.toFixed(1); + if (num % 1 !== 0) return v.toFixed(1); + return Math.round(v).toString(); + }, [num, isK]); + + const startedRef = useRef(false); useEffect(() => { - const el = ref.current; - if (!el || !inView || !match) return; - let t0: number | null = null; - const tick = (ts: number) => { - if (!t0) t0 = ts; - const p = Math.min((ts - t0) / (duration * 1000), 1); - const ease = 1 - Math.pow(1 - p, 3); - el.textContent = `${match.prefix}${fmt(start + (num - start) * ease)}${match.suffix}`; - if (p < 1) requestAnimationFrame(tick); + const node = ref.current; + if (!node || !inView || startedRef.current) return; + startedRef.current = true; + const setText = (v: number) => { + node.textContent = `${isNeg ? '−' : ''}${fmt(v)}${suffix}`; }; - requestAnimationFrame(tick); - }, [inView, num, duration, fmt, start, match]); - - if (!match) { - return {value}; - } + if (prefersReducedMotion || document.visibilityState !== 'visible') { + setText(num); + return; + } + setText(startValue); + const clampedDuration = Math.min(Math.max(1.8, duration), 2); + const controls = animate(startValue, num, { + duration: clampedDuration, + ease: [0.16, 1, 0.3, 1] as [number, number, number, number], + onUpdate: (v: number) => setText(v), + }); + return () => controls.stop(); + }, [inView, num, duration, fmt, isNeg, suffix, prefersReducedMotion, startValue]); return ( - - {match.prefix} - {fmt(start)} - {match.suffix} + + {isNeg ? '−' : ''}{fmt(startValue)}{suffix} ); } -/* ═══════════════════════════════════════════════════════════════════════════ - ROOT COMPONENT - ═══════════════════════════════════════════════════════════════════════════ */ -export default function LandingPage() { - const [scrolled, setScrolled] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - const [user, setUser] = useState(null); - const [configured, setConfigured] = useState(true); - const localDev = isLocalSupabase(); +// ─── SectionHeader ─────────────────────────────────────────────────────────── + +function SectionHeader({ num, title }: { num: string; title: string }) { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true }); + const target = parseInt(num, 10); + const fmt = (v: number) => String(Math.round(v)).padStart(2, '0'); + const [display, setDisplay] = useState(fmt(target)); + const startedRef = useRef(false); useEffect(() => { - const fn = () => setScrolled(window.scrollY > 20); - fn(); - window.addEventListener('scroll', fn, { passive: true }); - return () => window.removeEventListener('scroll', fn); - }, []); + if (!inView || startedRef.current) return; + startedRef.current = true; + if (document.visibilityState !== 'visible') { setDisplay(fmt(target)); return; } + setDisplay('00'); + const controls = animate(0, target, { + duration: 0.4, + onUpdate: (v: number) => setDisplay(fmt(v)), + }); + return () => controls.stop(); + }, [inView, target]); + + return ( +
    +
    {display} / {title.split(' ')[0]}
    +
    {title}
    +
    + ); +} + +// ─── SectionCurtain ────────────────────────────────────────────────────────── + +function SectionCurtain({ children, dark, className = '' }: { + children: React.ReactNode; dark?: boolean; className?: string; +}) { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); + const [phase, setPhase] = useState<'idle' | 'wiping' | 'done'>('idle'); useEffect(() => { - document.body.style.overflow = menuOpen ? 'hidden' : ''; - return () => { - document.body.style.overflow = ''; - }; - }, [menuOpen]); + if (inView && phase === 'idle') { + setPhase('wiping'); + const t = setTimeout(() => setPhase('done'), 900); + return () => clearTimeout(t); + } + }, [inView, phase]); + + return ( +
    + +
    {children}
    +
    + ); +} + +// ─── Nav ───────────────────────────────────────────────────────────────────── + +// Local Supabase doesn't have the GitHub OAuth provider enabled (we don't +// commit a config.toml with a client secret). On a contributor's laptop, the +// "Get Started" button has to route to /dev/login instead — same flow the +// dev-login page itself uses. On prod (mergeship.dev → *.supabase.co) the +// real OAuth flow still runs. +function isLocalSupabase(): boolean { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; + return url.includes('127.0.0.1') || url.includes('localhost'); +} + +function NavAuth() { + const [user, setUser] = useState(null); + const [configured, setConfigured] = useState(true); + const localDev = isLocalSupabase(); useEffect(() => { const sb = getBrowserSupabase(); @@ -128,21 +240,18 @@ export default function LandingPage() { const u = data.user; const meta = (u.user_metadata ?? {}) as Record; const name = - (meta['name'] as string | undefined) ?? - (meta['user_name'] as string | undefined) ?? - null; + (meta['name'] as string | undefined) ?? (meta['user_name'] as string | undefined) ?? null; setUser({ name, email: u.email ?? null }); }); }, []); const handleLogin = () => { + const origin = window.location.origin; const sb = getBrowserSupabase(); if (!sb) return; void sb.auth.signInWithOAuth({ provider: 'github', - options: { - redirectTo: `${window.location.origin}/api/auth/callback?next=/dashboard`, - }, + options: { redirectTo: `${origin}/api/auth/callback?next=/dashboard` }, }); }; @@ -153,28 +262,40 @@ export default function LandingPage() { setUser(null); }; - /* helper to render the primary CTA depending on auth state */ - const PrimaryCTA = ({ label, className = 'btn-neon' }: { label: string; className?: string }) => { - if (user) { - return ( - - {label} - - ); - } - if (localDev) { - return ( - - {label} - - ); - } + if (!configured) { return ( - ); - }; + } + + if (user) { + return ( +
    + + {user.name || user.email} + + + Dashboard → + + +
    + ); + } + if (localDev) { + return ( + + Sign in (dev) → + + ); + } return ( @@ -183,112 +304,97 @@ export default function LandingPage() { ); } - +function Nav() { + const [scrolled, setScrolled] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 20); + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); -
    - {!configured ? ( - - Sign-in coming soon - - ) : user ? ( - <> - {user.name || user.email} - Dashboard - - - ) : ( - <> - {localDev ? ( - Login - ) : ( - - )} - - - )} -
    + useEffect(() => { + if (mobileMenuOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [mobileMenuOpen]); - - - - {/* mobile menu */} - {menuOpen && ( - <> -
    setMenuOpen(false)} /> -
    - setMenuOpen(false)}>Platform - setMenuOpen(false)}>Features - setMenuOpen(false)}>Docs - setMenuOpen(false)}>Pricing -
    - {!configured ? ( - - Sign-in coming soon - - ) : user ? ( - <> - setMenuOpen(false)}> - Dashboard - - - - ) : ( - <> - {localDev ? ( - setMenuOpen(false)}>Login - ) : ( - - )} - - - )} -
    - + return ( + + ); +} + +// ─── Hero ──────────────────────────────────────────────────────────────────── + +function Hero() { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ target: ref, offset: ['start start', 'end start'] }); + const [sp, setSp] = useState(0); + useEffect(() => scrollYProgress.on('change', setSp), [scrollYProgress]); - {/* ════════ METRICS ═════════════════════════════════════════════════= */} -
    -
    -
    -
    PRs Managed
    + return ( +
    +
    +
    +
    @@ -336,117 +442,325 @@ export default function LandingPage() { and lets peer-verified PRs reach you pre-checked. - Connect Your Org → + Connect Your Org → see the dashboard → -
    -
    -
    Triage Latency
    -
    -
    -
    -
    Uptime SLA
    -
    -
    -
    Configuration
    + +
    Orgs onboarded
    +
    PRs routed
    +
    Review noise
    +
    +
    +
    + ); +} + +// ─── Ticker ────────────────────────────────────────────────────────────────── + +function Ticker() { + const items = [ + { tag: 'PR #1234 MERGED', body: 'L3 MENTOR VERIFIED', red: false }, + { tag: 'AI-GENERATED PR DETECTED', body: 'AUTO-FLAGGED — kyverno/chainsaw', red: true }, + { tag: 'NEW CONTRIBUTOR ONBOARDED', body: '@aria.dev → LEVEL 1', red: false }, + { tag: 'PR #4827 L2 VERIFIED', body: 'envoyproxy/gateway · 3m ago', red: false }, + { tag: 'LEVEL UP', body: '@hiro.k REACHED LEVEL 2', red: false }, + { tag: 'PR #9821 FLAGGED', body: 'LOW-QUALITY DUPLICATE', red: true }, + { tag: 'PR #7733 MERGED', body: 'L3 MENTOR VERIFIED — opentofu', red: false }, + { tag: 'MENTOR CHAIN COMPLETE', body: '4 PRs FAST-TRACKED', red: false }, + { tag: 'NEW ORG CONNECTED', body: 'kubernetes-sigs/karpenter', red: false }, + ]; + const doubled = [...items, ...items]; + return ( +
    +
    + {doubled.map((it, i) => ( + + {it.tag} + {it.body} + · + + ))} +
    +
    + ); +} + +// ─── Problem ───────────────────────────────────────────────────────────────── + +function Problem() { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-15%' }); + const left = [ + { t: 'No Knowledge', b: 'New contributors face a wall — repos with no roadmap, no entry points, no sense of where a beginner should start.' }, + { t: 'No Guided Path', b: 'Tutorials end at "fork the repo." Real contribution skills — review, scope, communication — are learned by accident, if at all.' }, + { t: 'No Community', b: 'Forums are dead. Discord is noise. Mentorship is a favor you have to ask for in DMs and rarely receive.' }, + ]; + const right = [ + { t: 'AI Slop PRs', b: 'Auto-generated diffs flood the queue. Maintainers waste hours triaging trash that looks plausible but adds nothing.' }, + { t: 'Scattered Data', b: 'Contributor trust, history, and skill live across GitHub, Discord, and memory. No unified signal to act on.' }, + { t: 'Too Much to Handle', b: 'A handful of maintainers absorb the cost of every contributor who shows up unprepared. Burnout is the default outcome.' }, + ]; + return ( + + +
    +
    +
    CONTRIBUTORS — LOST & UNSTRUCTURED
    + {left.map((p, i) => ( + +
    {p.t}
    +
    {p.b}
    +
    + ))}
    -
    - - {/* ════════ PAIN POINTS ═════════════════════════════════════════════= */} -
    -
    -

    Open source is broken for everyone.

    -

    - Maintainers drown in noise. Contributors struggle to build trust. We fix both. -

    +
    +
    MAINTAINERS — FLOODED & OVERWHELMED
    + {right.map((p, i) => ( + +
    {p.t}
    +
    {p.b}
    +
    + ))}
    +
    + + ); +} -
    -
    -
    -

    Maintainer Burnout

    -

    - Triaging low-quality PRs eats time and energy. Delayed reviews stall the whole project. -

    -
    -
    -
    -

    AI Spam

    -

    - Generative AI slop floods repositories. Maintainers waste hours on plausible-looking diffs that add nothing. -

    -
    -
    -
    -

    Steep Onboarding

    -

    - No guided path, no entry points. Eager contributors bounce before their first PR is even opened. -

    -
    -
    -
    - -
    -

    Stalled Velocity

    -

    - Verified contributions sit buried under unreviewed backlog. Development velocity grinds to a halt. -

    -
    -
    -
    +// ─── HowItWorks ────────────────────────────────────────────────────────────── + +function HowItWorks() { + const [active, setActive] = useState(0); + const handleTabClick = (index: number) => { + setActive(index); + const element = document.getElementById('how'); + if (element) { + const offset = 64; // height of the navbar + const bodyRect = document.body.getBoundingClientRect().top; + const elementRect = element.getBoundingClientRect().top; + const elementPosition = elementRect - bodyRect; + const offsetPosition = elementPosition - offset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + } + }; - {/* ════════ TRIAGE QUEUE ════════════════════════════════════════════= */} -
    -
    -

    The Triage Queue

    + const flows = [ + { + name: 'CONTRIBUTOR FLOW', + steps: [ + { t: 'Sign in with GitHub', b: 'A profile scan reads your existing contributions and auto-places you at the right starting level. No quiz, no LinkedIn skill grid.', tag: 'AUTO-PLACEMENT' }, + { t: 'Get placed at the right level', b: 'Smart onboarding caps new accounts at Level 2 — even if your GitHub history is strong. Trust is earned inside the system.', tag: 'L0 — L2 ENTRY' }, + { t: 'Work issues, get mentored', b: 'Every PR you open is reviewed by an L2 or L3 mentor before it touches the maintainer queue. Hierarchical peer review, baked in.', tag: 'PEER REVIEW' }, + { t: 'Earn, level up, unlock harder work', b: 'XP, badges, and a verifiable portfolio. Higher levels unlock harder issues and the ability to mentor others.', tag: 'PROGRESSION' }, + ], + }, + { + name: 'MAINTAINER FLOW', + steps: [ + { t: 'Connect your org', b: 'One OAuth flow pulls in every repo, contributor, and PR. Existing labels and CODEOWNERS are respected, not replaced.', tag: 'GITHUB OAUTH' }, + { t: 'Define gates per repo', b: 'Decide which level can touch which directories. Lock the docs/ folder to L1+, the core to L3+ — granularity without overhead.', tag: 'ACCESS GATES' }, + { t: 'Receive pre-checked PRs', b: 'PRs arrive with a Trust Score and mentor sign-off. AI-flagged submissions never reach your inbox.', tag: 'TRUST SCORE' }, + { t: 'Grow your reviewer pool', b: 'Promising contributors are surfaced — promote them to L3 with one click and let them carry review weight.', tag: 'DELEGATION' }, + ], + }, + { + name: 'THE FLYWHEEL', + steps: [ + { t: 'Contributors land prepared', b: 'New developers arrive with a path, not a Discord ping. Day one is productive, not exhausting.', tag: 'INPUT' }, + { t: 'Mentors carry the review load', b: 'Mid-level contributors review junior PRs. Senior contributors review mid-level reviews. Pressure spreads.', tag: 'DISTRIBUTION' }, + { t: 'Maintainers gain leverage', b: 'A small core ships more by reviewing less. Energy returns to the work that only they can do.', tag: 'LEVERAGE' }, + { t: 'The community compounds', b: 'Trained L2s become L3s. L3s become maintainers. The pipeline is the product.', tag: 'COMPOUNDING' }, + ], + }, + ]; + return ( + + +
    +
    + {flows.map((f, i) => ( + + ))}
    -

    - Context-aware routing ensures the right eyes are on the right code. -

    - -
    -
    -
    - - - -
    -
    triage_queue
    -
    -
    -
    - -
    -
    Refactor core routing engine
    -
    #1892 opened 8 hours ago by @ryan-lewis
    -
    -
    - L3 Expert - Tests Passed -
    -
    -
    - -
    -
    Add guards and fallback
    -
    #1893 opened 12 hours ago by @anonymous
    -
    -
    - AI Flagged - CI Fail -
    -
    -
    - -
    -
    Update documentation types in README
    -
    #1894 opened 1 day ago by @contrib-bot
    -
    -
    - L1 Triaged +
    + {flows[active].steps.map((s, i) => ( + +
    {String(i + 1).padStart(2, '0')}
    +
    +
    {s.t}
    +
    {s.b}
    +
    + + {s.tag} +
    +
    + ))} +
    +
    + + ); +} + +// ─── Levels ────────────────────────────────────────────────────────────────── + +function Levels() { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); + const cards = [ + { n: 'L0', t: 'Newcomer', d: '5-day course only. No repo access until the orientation track is complete.', a: 'COURSE ONLY', p: 25 }, + { n: 'L1', t: 'Contributor', d: 'Basic issues — bugs, docs, low-risk patches. Mentored review on every PR.', a: 'BASIC ISSUES', p: 50 }, + { n: 'L2', t: 'Practitioner', d: 'Intermediate issues. Eligible to review L1 PRs and contribute to mentorship chains.', a: 'INTERMEDIATE + REVIEW', p: 75 }, + { n: 'L3', t: 'Expert', d: 'Advanced issues, core code paths, mentor privileges, and trust-score weighting on reviews.', a: 'ADVANCED + MENTOR', p: 100 }, + ]; + return ( + + +
    + {cards.map((c, i) => ( + +
    {c.n}
    +
    {c.t}
    +
    {c.d}
    +
    → {c.a}
    +
    +
    +
    + ))} +
    + + NOTE + Level 3 cannot be imported from GitHub. It requires actually solving Level 2 issues inside MergeShip — so it stays fair and cannot be gamed. + +
    + ); +} + +// ─── Mentorship ────────────────────────────────────────────────────────────── + +function Mentorship() { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-15%' }); + + const nodes = [ + { l: 'L1 CONTRIBUTOR', d: 'Submits the initial PR. Tagged with their level and Trust Score.', green: false }, + { l: 'L2 MENTOR REVIEWS', d: 'Reviews diff, unblocks the contributor, tags as verified.', green: true }, + { l: 'L3 MENTOR REVIEWS', d: 'For complex work, deepens verification and signs off on architecture.', green: true }, + { l: 'MAINTAINER RECEIVES', d: 'PR arrives pre-tagged. Review is fast-tracked, often a single approval away.', green: false }, + ]; + const prs = [ + { n: '#1234', t: 'fix: cleanup error in file handler', m: 'kyverno/chainsaw · 2h ago', b: 'L3 VERIFIED', c: 'verified-strong', bc: 'badge-green' }, + { n: '#1235', t: 'feat: skip step based on condition', m: 'kyverno/chainsaw · 4h ago', b: 'L2 VERIFIED', c: 'verified', bc: 'badge-green' }, + { n: '#1236', t: 'update README.md typos', m: 'kyverno/chainsaw · 6h ago', b: 'L1 · UNVERIFIED', c: 'muted', bc: 'badge-muted' }, + { n: '#1237', t: 'optimize entire codebase performance', m: 'kyverno/chainsaw · 1d ago', b: 'AI FLAGGED', c: 'flagged', bc: 'badge-red' }, + ]; + + return ( + + +
    +
    + + Mentorship is the infrastructure, not the favor. + + + Every PR walks up a chain of trust before it reaches a maintainer. The contributor learns. + The reviewer leads. The maintainer gets a clean diff and a signed-off context. + +
    + + + + {nodes.map((n, i) => ( + +
    {n.l}
    +
    {n.d}
    +
    + ))} +
    +
    +
    +
    MAINTAINER QUEUE — kyverno/chainsaw
    +
    + {prs.map((p, i) => ( + +
    {p.n} — {p.t}
    +
    {p.m}
    + {p.b} +
    + ))}
    @@ -527,7 +841,7 @@ function CtaSplit() {

    Connect your org. Review with confidence.

    - - {/* ════════ CTA BANNER ═════════════════════════════════════════════= */} -
    -
    -

    Ready to ship better?

    -

    - Join hundreds of developers and maintainers building clean, verified, high-velocity open source. -

    - -
    -
    - {/* ════════ FOOTER ═════════════════════════════════════════════════= */} -
    -
    -
    - © 2026 MergeShip. Built for performance. -
    - +function Footer() { + const ref = useRef(null); + const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); + return ( + +
    +
    +
    © 2026 MergeShip Labs
    +
    SHIP / VERIFY / MERGE
    +
    +
    + ); +} + +// ─── Root ───────────────────────────────────────────────────────────────────── + +export default function LandingPage() { + const [forceReveal, setForceReveal] = useState(false); + useEffect(() => { + const t = setTimeout(() => setForceReveal(true), 2200); + return () => clearTimeout(t); + }, []); + + return ( +
    +
    ); } From fc184bea394dd203b2415055c51116c30900e9e9 Mon Sep 17 00:00:00 2001 From: riddhima Date: Thu, 11 Jun 2026 14:04:48 +0530 Subject: [PATCH 5/7] fix(onboarding): route primary CTAs to onboarding --- src/components/landing/LandingPage.tsx | 1161 +++++++----------------- 1 file changed, 345 insertions(+), 816 deletions(-) diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index 43125ac..2b0ad3a 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -1,233 +1,121 @@ -/* eslint-disable */ -// @ts-nocheck — partner's landing page; backend rebuild keeps it untouched except for auth swap 'use client'; import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { motion, useScroll, animate, useReducedMotion } from 'framer-motion'; -import dynamic from 'next/dynamic'; import Link from 'next/link'; +import { + AlertTriangle, + Bot, + Zap, + Lock, + Clock, + Menu, + X, + ArrowRight, + ChevronRight, +} from 'lucide-react'; import { getBrowserSupabase } from '@/lib/supabase/browser'; import '@/app/landing.css'; type NavUser = { name: string | null; email: string | null }; -const HeroScene = dynamic(() => import('./HeroScene'), { ssr: false }); - -// ─── Shared hook ──────────────────────────────────────────────────────────── +function isLocalSupabase(): boolean { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; + return url.includes('127.0.0.1') || url.includes('localhost'); +} -function useInView(ref: React.RefObject, opts: { once?: boolean; margin?: string; fallbackMs?: number } = {}) { - const { once = true, margin = '0px', fallbackMs = 1800 } = opts; +/* ── Animated counter ──────────────────────────────────────────────────── */ +function StatNumber({ value, duration = 2 }: { value: string; duration?: number }) { + const ref = useRef(null); const [inView, setInView] = useState(false); + + const match = useMemo(() => { + const m = /^([^0-9.]*)([0-9.]+)(.*)$/.exec(value); + if (!m) return null; + return { + prefix: m[1] ?? '', + num: parseFloat(m[2] ?? '0'), + suffix: m[3] ?? '', + }; + }, [value]); + useEffect(() => { if (!ref.current) return; - let done = false; - const reveal = () => { if (!done) { done = true; setInView(true); } }; const io = new IntersectionObserver( (entries) => { - for (const e of entries) { - if (e.isIntersecting) { reveal(); if (once) io.disconnect(); } - else if (!once) setInView(false); + const e = entries[0]; + if (e && e.isIntersecting) { + setInView(true); + io.disconnect(); } }, - { rootMargin: margin, threshold: 0.05 } + { threshold: 0.15 }, ); io.observe(ref.current); - const t = setTimeout(reveal, fallbackMs); - return () => { io.disconnect(); clearTimeout(t); }; + return () => io.disconnect(); }, []); - return inView; -} -// ─── FadeUp ────────────────────────────────────────────────────────────────── + const num = match ? match.num : 0; + const start = useMemo(() => (Number.isFinite(num) && num > 0 ? num * 0.3 : 0), [num]); -function FadeUp({ - delay = 0, y = 16, duration = 0.6, children, className = '', - style = {}, as: As = 'div', -}: { - delay?: number; y?: number; duration?: number; children?: React.ReactNode; - className?: string; style?: React.CSSProperties; as?: React.ElementType; -}) { - return ( - - {children} - + const fmt = useCallback( + (v: number) => { + if (num >= 1000) return Math.round(v).toLocaleString(); + if (num % 1 !== 0) return v.toFixed(1); + return Math.round(v).toString(); + }, + [num], ); -} - -// ─── SplitText ─────────────────────────────────────────────────────────────── - -function SplitText({ text, delay = 0 }: { text: string; delay?: number }) { - const words = text.split(' '); - let charIndex = 0; - const elements: React.ReactNode[] = []; - words.forEach((word, wi) => { - const isItalic = word.startsWith('*') && word.endsWith('*'); - const clean = isItalic ? word.slice(1, -1) : word; - elements.push( - - {clean.split('').map((c, ci) => { - const idx = charIndex++; - return ( - - {c} - - ); - })} - - ); - if (wi < words.length - 1) elements.push( ); - }); - return {elements}; -} - -// ─── StatNumber ────────────────────────────────────────────────────────────── - -function StatNumber({ value, duration = 2 }: { value: string; duration?: number }) { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); - const prefersReducedMotion = useReducedMotion(); - const isPercent = value.includes('%'); - const isNeg = value.startsWith('−'); - const isK = value.toLowerCase().endsWith('k'); - const num = parseFloat(value.replace(/[^0-9.]/g, '')); - const suffix = isPercent ? '%' : isK ? 'k' : ''; - const startValue = useMemo(() => { - if (!Number.isFinite(num) || num <= 0) return 0; - let hash = 0; - for (let i = 0; i < value.length; i++) { - hash = (hash * 31 + value.charCodeAt(i)) >>> 0; - } - const r = (hash % 1000) / 1000; - const minRatio = 0.25; - const maxRatio = 0.6; - const ratio = minRatio + r * (maxRatio - minRatio); - return num * ratio; - }, [num, value]); - - const fmt = useCallback((v: number) => { - if (num >= 1000) return Math.round(v).toLocaleString(); - if (isK) return v.toFixed(1); - if (num % 1 !== 0) return v.toFixed(1); - return Math.round(v).toString(); - }, [num, isK]); - - const startedRef = useRef(false); useEffect(() => { - const node = ref.current; - if (!node || !inView || startedRef.current) return; - startedRef.current = true; - const setText = (v: number) => { - node.textContent = `${isNeg ? '−' : ''}${fmt(v)}${suffix}`; + const el = ref.current; + if (!el || !inView || !match) return; + let t0: number | null = null; + const tick = (ts: number) => { + if (!t0) t0 = ts; + const p = Math.min((ts - t0) / (duration * 1000), 1); + const ease = 1 - Math.pow(1 - p, 3); + el.textContent = `${match.prefix}${fmt(start + (num - start) * ease)}${match.suffix}`; + if (p < 1) requestAnimationFrame(tick); }; - if (prefersReducedMotion || document.visibilityState !== 'visible') { - setText(num); - return; - } - setText(startValue); - const clampedDuration = Math.min(Math.max(1.8, duration), 2); - const controls = animate(startValue, num, { - duration: clampedDuration, - ease: [0.16, 1, 0.3, 1] as [number, number, number, number], - onUpdate: (v: number) => setText(v), - }); - return () => controls.stop(); - }, [inView, num, duration, fmt, isNeg, suffix, prefersReducedMotion, startValue]); + requestAnimationFrame(tick); + }, [inView, num, duration, fmt, start, match]); + + if (!match) { + return {value}; + } return ( - - {isNeg ? '−' : ''}{fmt(startValue)}{suffix} + + {match.prefix} + {fmt(start)} + {match.suffix} ); } -// ─── SectionHeader ─────────────────────────────────────────────────────────── - -function SectionHeader({ num, title }: { num: string; title: string }) { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true }); - const target = parseInt(num, 10); - const fmt = (v: number) => String(Math.round(v)).padStart(2, '0'); - const [display, setDisplay] = useState(fmt(target)); - const startedRef = useRef(false); +/* ═══════════════════════════════════════════════════════════════════════════ + ROOT COMPONENT + ═══════════════════════════════════════════════════════════════════════════ */ +export default function LandingPage() { + const [scrolled, setScrolled] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [user, setUser] = useState(null); + const [configured, setConfigured] = useState(true); + const localDev = isLocalSupabase(); useEffect(() => { - if (!inView || startedRef.current) return; - startedRef.current = true; - if (document.visibilityState !== 'visible') { setDisplay(fmt(target)); return; } - setDisplay('00'); - const controls = animate(0, target, { - duration: 0.4, - onUpdate: (v: number) => setDisplay(fmt(v)), - }); - return () => controls.stop(); - }, [inView, target]); - - return ( -
    -
    {display} / {title.split(' ')[0]}
    -
    {title}
    -
    - ); -} - -// ─── SectionCurtain ────────────────────────────────────────────────────────── - -function SectionCurtain({ children, dark, className = '' }: { - children: React.ReactNode; dark?: boolean; className?: string; -}) { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); - const [phase, setPhase] = useState<'idle' | 'wiping' | 'done'>('idle'); + const fn = () => setScrolled(window.scrollY > 20); + fn(); + window.addEventListener('scroll', fn, { passive: true }); + return () => window.removeEventListener('scroll', fn); + }, []); useEffect(() => { - if (inView && phase === 'idle') { - setPhase('wiping'); - const t = setTimeout(() => setPhase('done'), 900); - return () => clearTimeout(t); - } - }, [inView, phase]); - - return ( -
    - -
    {children}
    -
    - ); -} - -// ─── Nav ───────────────────────────────────────────────────────────────────── - -// Local Supabase doesn't have the GitHub OAuth provider enabled (we don't -// commit a config.toml with a client secret). On a contributor's laptop, the -// "Get Started" button has to route to /dev/login instead — same flow the -// dev-login page itself uses. On prod (mergeship.dev → *.supabase.co) the -// real OAuth flow still runs. -function isLocalSupabase(): boolean { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''; - return url.includes('127.0.0.1') || url.includes('localhost'); -} - -function NavAuth() { - const [user, setUser] = useState(null); - const [configured, setConfigured] = useState(true); - const localDev = isLocalSupabase(); + document.body.style.overflow = menuOpen ? 'hidden' : ''; + return () => { + document.body.style.overflow = ''; + }; + }, [menuOpen]); useEffect(() => { const sb = getBrowserSupabase(); @@ -240,18 +128,21 @@ function NavAuth() { const u = data.user; const meta = (u.user_metadata ?? {}) as Record; const name = - (meta['name'] as string | undefined) ?? (meta['user_name'] as string | undefined) ?? null; + (meta['name'] as string | undefined) ?? + (meta['user_name'] as string | undefined) ?? + null; setUser({ name, email: u.email ?? null }); }); }, []); const handleLogin = () => { - const origin = window.location.origin; const sb = getBrowserSupabase(); if (!sb) return; void sb.auth.signInWithOAuth({ provider: 'github', - options: { redirectTo: `${origin}/api/auth/callback?next=/dashboard` }, + options: { + redirectTo: `${window.location.origin}/api/auth/callback?next=/dashboard`, + }, }); }; @@ -262,655 +153,293 @@ function NavAuth() { setUser(null); }; - if (!configured) { - return ( - - ); - } - - if (user) { - return ( -
    - - {user.name || user.email} - - - Dashboard → + /* helper to render the primary CTA depending on auth state */ + const PrimaryCTA = ({ label, className = 'btn-neon' }: { label: string; className?: string }) => { + if (user) { + return ( + + {label} - -
    - ); - } - if (localDev) { + ); + } + return ( - - Sign in (dev) → + + {label} ); - } + }; return ( - - Get Started → - - ); -} +
    + {/* ambient glow behind hero */} +
    + + {/* ════════ NAVBAR ════════════════════════════════════════════════════ */} + - ); -} + -// ─── Hero ──────────────────────────────────────────────────────────────────── - -function Hero() { - const ref = useRef(null); - const { scrollYProgress } = useScroll({ target: ref, offset: ['start start', 'end start'] }); - const [sp, setSp] = useState(0); - useEffect(() => scrollYProgress.on('change', setSp), [scrollYProgress]); - - return ( -
    -
    -
    - + {/* ════════ METRICS ═════════════════════════════════════════════════= */} +
    +
    +
    +
    PRs Managed
    -
    - - - FOR CONTRIBUTORS - -

    - - -

    - - Most contributors fail before they start. No path. No community. No feedback. - MergeShip changes that — level by level, PR by PR. - - - - Start Contributing → - - see how it works → - +
    +
    +
    Triage Latency
    - -
    Active contributors
    -
    First PR merged
    -
    Skill levels
    -
    -
    - -
    -
    - - - FOR MAINTAINERS - -

    - - - - {' '} - - -

    - - A smart command center that surfaces what matters, buries the noise, - and lets peer-verified PRs reach you pre-checked. - - - Connect Your Org → - see the dashboard → - +
    +
    +
    Uptime SLA
    - -
    Orgs onboarded
    -
    PRs routed
    -
    Review noise
    -
    -
    -
    - ); -} - -// ─── Ticker ────────────────────────────────────────────────────────────────── - -function Ticker() { - const items = [ - { tag: 'PR #1234 MERGED', body: 'L3 MENTOR VERIFIED', red: false }, - { tag: 'AI-GENERATED PR DETECTED', body: 'AUTO-FLAGGED — kyverno/chainsaw', red: true }, - { tag: 'NEW CONTRIBUTOR ONBOARDED', body: '@aria.dev → LEVEL 1', red: false }, - { tag: 'PR #4827 L2 VERIFIED', body: 'envoyproxy/gateway · 3m ago', red: false }, - { tag: 'LEVEL UP', body: '@hiro.k REACHED LEVEL 2', red: false }, - { tag: 'PR #9821 FLAGGED', body: 'LOW-QUALITY DUPLICATE', red: true }, - { tag: 'PR #7733 MERGED', body: 'L3 MENTOR VERIFIED — opentofu', red: false }, - { tag: 'MENTOR CHAIN COMPLETE', body: '4 PRs FAST-TRACKED', red: false }, - { tag: 'NEW ORG CONNECTED', body: 'kubernetes-sigs/karpenter', red: false }, - ]; - const doubled = [...items, ...items]; - return ( -
    -
    - {doubled.map((it, i) => ( - - {it.tag} - {it.body} - · - - ))} -
    -
    - ); -} - -// ─── Problem ───────────────────────────────────────────────────────────────── - -function Problem() { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-15%' }); - const left = [ - { t: 'No Knowledge', b: 'New contributors face a wall — repos with no roadmap, no entry points, no sense of where a beginner should start.' }, - { t: 'No Guided Path', b: 'Tutorials end at "fork the repo." Real contribution skills — review, scope, communication — are learned by accident, if at all.' }, - { t: 'No Community', b: 'Forums are dead. Discord is noise. Mentorship is a favor you have to ask for in DMs and rarely receive.' }, - ]; - const right = [ - { t: 'AI Slop PRs', b: 'Auto-generated diffs flood the queue. Maintainers waste hours triaging trash that looks plausible but adds nothing.' }, - { t: 'Scattered Data', b: 'Contributor trust, history, and skill live across GitHub, Discord, and memory. No unified signal to act on.' }, - { t: 'Too Much to Handle', b: 'A handful of maintainers absorb the cost of every contributor who shows up unprepared. Burnout is the default outcome.' }, - ]; - return ( - - -
    -
    -
    CONTRIBUTORS — LOST & UNSTRUCTURED
    - {left.map((p, i) => ( - -
    {p.t}
    -
    {p.b}
    -
    - ))} +
    +
    +
    Configuration
    -
    -
    MAINTAINERS — FLOODED & OVERWHELMED
    - {right.map((p, i) => ( - -
    {p.t}
    -
    {p.b}
    -
    - ))} +
    + + {/* ════════ PAIN POINTS ═════════════════════════════════════════════= */} +
    +
    +

    Open source is broken for everyone.

    +

    + Maintainers drown in noise. Contributors struggle to build trust. We fix both. +

    -
    - - ); -} - -// ─── HowItWorks ────────────────────────────────────────────────────────────── - -function HowItWorks() { - const [active, setActive] = useState(0); - const handleTabClick = (index: number) => { - setActive(index); - const element = document.getElementById('how'); - if (element) { - const offset = 64; // height of the navbar - const bodyRect = document.body.getBoundingClientRect().top; - const elementRect = element.getBoundingClientRect().top; - const elementPosition = elementRect - bodyRect; - const offsetPosition = elementPosition - offset; - - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth', - }); - } - }; - const flows = [ - { - name: 'CONTRIBUTOR FLOW', - steps: [ - { t: 'Sign in with GitHub', b: 'A profile scan reads your existing contributions and auto-places you at the right starting level. No quiz, no LinkedIn skill grid.', tag: 'AUTO-PLACEMENT' }, - { t: 'Get placed at the right level', b: 'Smart onboarding caps new accounts at Level 2 — even if your GitHub history is strong. Trust is earned inside the system.', tag: 'L0 — L2 ENTRY' }, - { t: 'Work issues, get mentored', b: 'Every PR you open is reviewed by an L2 or L3 mentor before it touches the maintainer queue. Hierarchical peer review, baked in.', tag: 'PEER REVIEW' }, - { t: 'Earn, level up, unlock harder work', b: 'XP, badges, and a verifiable portfolio. Higher levels unlock harder issues and the ability to mentor others.', tag: 'PROGRESSION' }, - ], - }, - { - name: 'MAINTAINER FLOW', - steps: [ - { t: 'Connect your org', b: 'One OAuth flow pulls in every repo, contributor, and PR. Existing labels and CODEOWNERS are respected, not replaced.', tag: 'GITHUB OAUTH' }, - { t: 'Define gates per repo', b: 'Decide which level can touch which directories. Lock the docs/ folder to L1+, the core to L3+ — granularity without overhead.', tag: 'ACCESS GATES' }, - { t: 'Receive pre-checked PRs', b: 'PRs arrive with a Trust Score and mentor sign-off. AI-flagged submissions never reach your inbox.', tag: 'TRUST SCORE' }, - { t: 'Grow your reviewer pool', b: 'Promising contributors are surfaced — promote them to L3 with one click and let them carry review weight.', tag: 'DELEGATION' }, - ], - }, - { - name: 'THE FLYWHEEL', - steps: [ - { t: 'Contributors land prepared', b: 'New developers arrive with a path, not a Discord ping. Day one is productive, not exhausting.', tag: 'INPUT' }, - { t: 'Mentors carry the review load', b: 'Mid-level contributors review junior PRs. Senior contributors review mid-level reviews. Pressure spreads.', tag: 'DISTRIBUTION' }, - { t: 'Maintainers gain leverage', b: 'A small core ships more by reviewing less. Energy returns to the work that only they can do.', tag: 'LEVERAGE' }, - { t: 'The community compounds', b: 'Trained L2s become L3s. L3s become maintainers. The pipeline is the product.', tag: 'COMPOUNDING' }, - ], - }, - ]; - return ( - - -
    -
    - {flows.map((f, i) => ( - - ))} -
    -
    - {flows[active].steps.map((s, i) => ( - -
    {String(i + 1).padStart(2, '0')}
    -
    -
    {s.t}
    -
    {s.b}
    -
    - - {s.tag} -
    -
    -
    - ))} +
    +
    +
    +

    Maintainer Burnout

    +

    + Triaging low-quality PRs eats time and energy. Delayed reviews stall the whole project. +

    +
    +
    +
    +

    AI Spam

    +

    + Generative AI slop floods repositories. Maintainers waste hours on plausible-looking diffs that add nothing. +

    +
    +
    +
    +

    Steep Onboarding

    +

    + No guided path, no entry points. Eager contributors bounce before their first PR is even opened. +

    +
    +
    +
    + +
    +

    Stalled Velocity

    +

    + Verified contributions sit buried under unreviewed backlog. Development velocity grinds to a halt. +

    +
    -
    - - ); -} +
    -// ─── Levels ────────────────────────────────────────────────────────────────── - -function Levels() { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); - const cards = [ - { n: 'L0', t: 'Newcomer', d: '5-day course only. No repo access until the orientation track is complete.', a: 'COURSE ONLY', p: 25 }, - { n: 'L1', t: 'Contributor', d: 'Basic issues — bugs, docs, low-risk patches. Mentored review on every PR.', a: 'BASIC ISSUES', p: 50 }, - { n: 'L2', t: 'Practitioner', d: 'Intermediate issues. Eligible to review L1 PRs and contribute to mentorship chains.', a: 'INTERMEDIATE + REVIEW', p: 75 }, - { n: 'L3', t: 'Expert', d: 'Advanced issues, core code paths, mentor privileges, and trust-score weighting on reviews.', a: 'ADVANCED + MENTOR', p: 100 }, - ]; - return ( - - -
    - {cards.map((c, i) => ( - -
    {c.n}
    -
    {c.t}
    -
    {c.d}
    -
    → {c.a}
    -
    - + {/* ════════ TRIAGE QUEUE ════════════════════════════════════════════= */} +
    +
    +

    The Triage Queue

    +
    +

    + Context-aware routing ensures the right eyes are on the right code. +

    + +
    +
    +
    + + +
    - - ))} -
    - - NOTE - Level 3 cannot be imported from GitHub. It requires actually solving Level 2 issues inside MergeShip — so it stays fair and cannot be gamed. - - - ); -} - -// ─── Mentorship ────────────────────────────────────────────────────────────── - -function Mentorship() { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-15%' }); - - const nodes = [ - { l: 'L1 CONTRIBUTOR', d: 'Submits the initial PR. Tagged with their level and Trust Score.', green: false }, - { l: 'L2 MENTOR REVIEWS', d: 'Reviews diff, unblocks the contributor, tags as verified.', green: true }, - { l: 'L3 MENTOR REVIEWS', d: 'For complex work, deepens verification and signs off on architecture.', green: true }, - { l: 'MAINTAINER RECEIVES', d: 'PR arrives pre-tagged. Review is fast-tracked, often a single approval away.', green: false }, - ]; - const prs = [ - { n: '#1234', t: 'fix: cleanup error in file handler', m: 'kyverno/chainsaw · 2h ago', b: 'L3 VERIFIED', c: 'verified-strong', bc: 'badge-green' }, - { n: '#1235', t: 'feat: skip step based on condition', m: 'kyverno/chainsaw · 4h ago', b: 'L2 VERIFIED', c: 'verified', bc: 'badge-green' }, - { n: '#1236', t: 'update README.md typos', m: 'kyverno/chainsaw · 6h ago', b: 'L1 · UNVERIFIED', c: 'muted', bc: 'badge-muted' }, - { n: '#1237', t: 'optimize entire codebase performance', m: 'kyverno/chainsaw · 1d ago', b: 'AI FLAGGED', c: 'flagged', bc: 'badge-red' }, - ]; - - return ( - - -
    -
    - - Mentorship is the infrastructure, not the favor. - - - Every PR walks up a chain of trust before it reaches a maintainer. The contributor learns. - The reviewer leads. The maintainer gets a clean diff and a signed-off context. - -
    - - - - {nodes.map((n, i) => ( - -
    {n.l}
    -
    {n.d}
    -
    - ))} +
    triage_queue
    -
    -
    -
    MAINTAINER QUEUE — kyverno/chainsaw
    -
    - {prs.map((p, i) => ( - -
    {p.n} — {p.t}
    -
    {p.m}
    - {p.b} -
    - ))} +
    +
    + +
    +
    Refactor core routing engine
    +
    #1892 opened 8 hours ago by @ryan-lewis
    +
    +
    + L3 Expert + Tests Passed +
    +
    +
    + +
    +
    Add guards and fallback
    +
    #1893 opened 12 hours ago by @anonymous
    +
    +
    + AI Flagged + CI Fail +
    +
    +
    + +
    +
    Update documentation types in README
    +
    #1894 opened 1 day ago by @contrib-bot
    +
    +
    + L1 Triaged +
    +
    -
    - - ); -} - -// ─── Comparison ────────────────────────────────────────────────────────────── - -function Comparison() { - const rows: [string, boolean, boolean, boolean][] = [ - ['Level-by-level issue unlocking', true, false, false], - ['GitHub profile auto-placement', true, false, false], - ['Hierarchical peer mentorship', true, false, false], - ['Contributor Trust Score on PRs', true, false, false], - ['AI-generated PR detection', true, false, true], - ['Smart PR queue by trust', true, false, false], - ['Verifiable open source portfolio', true, false, true], - ['Unified contributor + maintainer', true, false, false], - ]; - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); - - return ( - - - - - - - - - - - - - {rows.map((r, i) => ( - - - - - - - ))} - -
    FeatureMergeShipGitHub NativeOthers
    {r[0]}{r[1] ? '✓' : '—'}{r[2] ? '✓' : '—'}{r[3] ? '✓' : '—'}
    -
    - ); -} - -// ─── CtaSplit ──────────────────────────────────────────────────────────────── - -function CtaSplit() { - return ( -
    -
    -
    FOR CONTRIBUTORS
    -

    Your first real contribution starts here.

    -
    - - Start Contributing → - -
    -
    -
    -
    FOR MAINTAINERS
    -

    Connect your org. Review with confidence.

    -
    - - Connect Your Org → - +
    + + {/* ════════ CTA BANNER ═════════════════════════════════════════════= */} +
    +
    +

    Ready to ship better?

    +

    + Join hundreds of developers and maintainers building clean, verified, high-velocity open source. +

    +
    -
    -
    - ); -} + -// ─── Footer ────────────────────────────────────────────────────────────────── - -function Footer() { - const ref = useRef(null); - const inView = useInView(ref as React.RefObject, { once: true, margin: '-10%' }); - return ( -
    -
    -
    - - MergeShip - -
    Helping contributors learn the right way. Helping maintainers stay sane.
    -
    -
    - Docs - Pricing - Changelog - GitHub - Status + {/* ════════ FOOTER ═════════════════════════════════════════════════= */} +
    -
    -
    © 2026 MergeShip Labs
    -
    SHIP / VERIFY / MERGE
    -
    -
    - ); -} - -// ─── Root ───────────────────────────────────────────────────────────────────── - -export default function LandingPage() { - const [forceReveal, setForceReveal] = useState(false); - useEffect(() => { - const t = setTimeout(() => setForceReveal(true), 2200); - return () => clearTimeout(t); - }, []); - - return ( -
    -
    ); -} +} \ No newline at end of file From ce7575ab589a618da3cacccfcacaa19984f17e24 Mon Sep 17 00:00:00 2001 From: Ayush Patel Date: Thu, 11 Jun 2026 18:38:18 +0530 Subject: [PATCH 6/7] fix(middleware): allow unauthenticated access to onboarding route --- src/middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middleware.ts b/src/middleware.ts index f741963..949fda2 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,6 +17,7 @@ import { readSupabaseEnv } from '@/lib/supabase/env'; const GATE_BYPASS_PREFIXES = [ '/install', + '/onboarding', '/api/auth', '/api/webhooks', '/api/inngest', From 020cb987098aae000e3ef8d911c9e7653620811e Mon Sep 17 00:00:00 2001 From: Ayush Patel Date: Thu, 11 Jun 2026 18:44:43 +0530 Subject: [PATCH 7/7] fix(auth): route landing get-started flows through authentication before onboarding --- src/app/dev/login/buttons.tsx | 10 ++++++++-- src/app/dev/login/page.tsx | 6 ++++-- src/components/landing/LandingPage.tsx | 17 +++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/app/dev/login/buttons.tsx b/src/app/dev/login/buttons.tsx index 86bd1bc..2648b4f 100644 --- a/src/app/dev/login/buttons.tsx +++ b/src/app/dev/login/buttons.tsx @@ -13,7 +13,13 @@ type Persona = { const DEV_PASSWORD = 'dev-password-only'; -export default function DevLoginButtons({ personas }: { personas: Persona[] }) { +export default function DevLoginButtons({ + personas, + next = '/dashboard', +}: { + personas: Persona[]; + next?: string; +}) { const router = useRouter(); const [pending, setPending] = useState(null); const [error, setError] = useState(null); @@ -37,7 +43,7 @@ export default function DevLoginButtons({ personas }: { personas: Persona[] }) { return; } router.refresh(); - router.push('/dashboard'); + router.push(next); } return ( diff --git a/src/app/dev/login/page.tsx b/src/app/dev/login/page.tsx index 78f342d..132f3e8 100644 --- a/src/app/dev/login/page.tsx +++ b/src/app/dev/login/page.tsx @@ -8,11 +8,13 @@ export const dynamic = 'force-dynamic'; * Used by contributors and CI to sign in as one of the seeded test users * without needing real GitHub OAuth. */ -export default function DevLoginPage() { +export default function DevLoginPage({ searchParams }: { searchParams: { next?: string } }) { if (process.env.NODE_ENV === 'production') { notFound(); } + const next = searchParams.next ?? '/dashboard'; + const personas = [ { email: 'alice@test.local', level: 'L0', label: 'Alice', blurb: 'Brand new, no audit yet' }, { email: 'bob@test.local', level: 'L1', label: 'Bob', blurb: 'Audited, has active recs' }, @@ -38,7 +40,7 @@ export default function DevLoginPage() {

    - +
    ); diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index 2b0ad3a..eaa9c8a 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -135,13 +135,14 @@ export default function LandingPage() { }); }, []); - const handleLogin = () => { + const handleLogin = (nextPath: string | unknown = '/dashboard') => { const sb = getBrowserSupabase(); if (!sb) return; + const next = typeof nextPath === 'string' ? nextPath : '/dashboard'; void sb.auth.signInWithOAuth({ provider: 'github', options: { - redirectTo: `${window.location.origin}/api/auth/callback?next=/dashboard`, + redirectTo: `${window.location.origin}/api/auth/callback?next=${encodeURIComponent(next)}`, }, }); }; @@ -163,10 +164,18 @@ export default function LandingPage() { ); } + if (localDev) { + return ( + + {label} + + ); + } + return ( - + ); };