From bf7d5e668ff2394659562908e7e755dc1fedac80 Mon Sep 17 00:00:00 2001 From: KonstantinMB Date: Mon, 13 Jul 2026 12:23:46 +0300 Subject: [PATCH 1/2] feat(map): dedicated /map page, homepage database preview, fix zoom-out gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New /map route (MapPage) showcases the interactive world map full-width with a page header; added to desktop More menu + mobile nav (World Map). - Homepage: replace the embedded interactive map with a compact, non-interactive database preview (latest YC companies + CTA to /database); keep a prominent 'Explore the interactive world map' link to /map. Repoint #map anchors and the bento 'Interactive Map' card to /map; hero 'Start Exploring' scrolls to the preview. - Fix the blue/navy band at the top when zoomed out: clamp the 2D view so the Web Mercator world always covers the viewport (min-zoom fills the larger dimension) and the poles can't be panned into view — no background gap in either theme or on mobile. Basemap-matched container bg as a backup. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01R4B1pcs7619oF8o4jn3PZF --- frontend/src/App.tsx | 2 + frontend/src/components/DatabasePreview.tsx | 129 ++++++++++++++++++ frontend/src/components/MobileBottomNav.tsx | 3 +- frontend/src/components/Navbar.tsx | 3 +- frontend/src/components/WorldMap/DeckMap.tsx | 33 ++++- frontend/src/components/WorldMap/WorldMap.tsx | 5 +- frontend/src/pages/HomePage.tsx | 45 +++--- frontend/src/pages/MapPage.tsx | 55 ++++++++ 8 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/DatabasePreview.tsx create mode 100644 frontend/src/pages/MapPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 28d0663..35877f4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { CompanyCardPage } from './pages/CompanyCardPage'; import { HiringBoardPagePaginated } from './pages/HiringBoardPagePaginated'; import { HiringAnalyticsPage } from './pages/HiringAnalyticsPage'; import { DatabasePage } from './pages/DatabasePage'; +import { MapPage } from './pages/MapPage'; import { PageTitleManager } from './components/PageTitleManager'; import { DevAuthProvider } from './contexts/DevAuthContext'; import { SignupPage } from './pages/SignupPage'; @@ -197,6 +198,7 @@ function AnimatedRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/DatabasePreview.tsx b/frontend/src/components/DatabasePreview.tsx new file mode 100644 index 0000000..79676d8 --- /dev/null +++ b/frontend/src/components/DatabasePreview.tsx @@ -0,0 +1,129 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import { ArrowRight, Database, ExternalLink, CheckCircle2 } from 'lucide-react'; +import { apiClient, type Company } from '../lib/api'; +import { CompanyLogo } from './ui/CompanyLogo'; +import { HackerCard } from './ui/hacker-card'; + +const PREVIEW_LIMIT = 8; + +// A lean, non-interactive teaser of the /database table — most recent YC +// companies. No filters or pagination; the CTA drives users to the full page. +export function DatabasePreview() { + const { data, isLoading } = useQuery({ + queryKey: ['database-preview'], + // No `source` param => YC-only (backend default); has_logo keeps rows tidy. + queryFn: () => + apiClient.getCompanies({ limit: PREVIEW_LIMIT, has_logo: true }).then((r) => r.data), + staleTime: 1000 * 60 * 5, + }); + + const companies: Company[] = data?.companies ?? []; + const total = data?.total ?? 0; + + return ( + + {/* Header */} +
+
+ + Latest YC companies + {total > 0 && ( + + {total.toLocaleString()} total + + )} +
+ + Open full database + + +
+ + {/* Table */} +
+ + + + + + + + + + + + + {isLoading && companies.length === 0 + ? Array.from({ length: PREVIEW_LIMIT }).map((_, i) => ( + + + + )) + : companies.map((c, i) => ( + + + + + + + + + ))} + +
CompanyBatchIndustryLocationTeamHiring
+
+
+ + + + {c.name} + + + {c.batch || '—'} + {c.industry || '—'} + + + {c.all_locations ? c.all_locations.split(';')[0].trim() : '—'} + + + {c.team_size ? c.team_size.toLocaleString() : '—'} + + {c.is_hiring ? ( + + ) : ( + + )} +
+
+ + {/* Footer CTA */} +
+ + Sortable, filterable, {total > 0 ? total.toLocaleString() : ''} companies — batch, industry, funding & more. + + + + Explore the database + +
+
+ ); +} diff --git a/frontend/src/components/MobileBottomNav.tsx b/frontend/src/components/MobileBottomNav.tsx index 2daec85..f782bf3 100644 --- a/frontend/src/components/MobileBottomNav.tsx +++ b/frontend/src/components/MobileBottomNav.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { - Home, BarChart3, Wrench, BookOpen, Map as MapIcon, DollarSign, Share2, Briefcase, + Home, BarChart3, Wrench, BookOpen, Map as MapIcon, Globe2, DollarSign, Share2, Briefcase, Mail, Database, Terminal, Menu, X, Moon, Sun, Command, LogOut, LayoutDashboard, } from 'lucide-react'; import { useApp } from '../contexts/AppContext'; @@ -28,6 +28,7 @@ const groups: { title: string; items: Item[] }[] = [ { title: 'More', items: [ + { label: 'World Map', icon: Globe2, path: '/map' }, { label: 'Tools', icon: Wrench, path: '/tools' }, { label: 'Founders', icon: BookOpen, path: '/founders' }, { label: 'Roadmap', icon: MapIcon, path: '/roadmap' }, diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 48b1aa3..890e0c5 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { - Home, BarChart3, Wrench, BookOpen, Map as MapIcon, DollarSign, Share2, + Home, BarChart3, Wrench, BookOpen, Map as MapIcon, Globe2, DollarSign, Share2, Moon, Sun, Command, Briefcase, Mail, Database, Terminal, ChevronDown, LayoutDashboard, LogOut, KeyRound, } from 'lucide-react'; @@ -27,6 +27,7 @@ const primaryTabs: NavTab[] = [ const showShareNav = import.meta.env.VITE_SHOW_SHARE_NAV === 'true' || import.meta.env.VITE_SHOW_SHARE_NAV === '1'; const moreTabs: NavTab[] = [ + { id: 'map', label: 'World Map', icon: Globe2, path: '/map' }, { id: 'funding', label: 'Funding', icon: DollarSign, path: '/funding' }, { id: 'tools', label: 'Tools', icon: Wrench, path: '/tools' }, { id: 'founders', label: 'Founders', icon: BookOpen, path: '/founders' }, diff --git a/frontend/src/components/WorldMap/DeckMap.tsx b/frontend/src/components/WorldMap/DeckMap.tsx index 63f4300..18b4ab0 100644 --- a/frontend/src/components/WorldMap/DeckMap.tsx +++ b/frontend/src/components/WorldMap/DeckMap.tsx @@ -129,6 +129,32 @@ function quantize(v: number): number { return Math.round(v * 20) / 20; } +const TILE_SIZE = 512; + +function latToMercY(lat: number): number { + const s = Math.sin((lat * Math.PI) / 180); + return 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI; +} +function mercYToLat(y: number): number { + const n = Math.PI - 2 * Math.PI * y; + return (Math.atan(Math.sinh(n)) * 180) / Math.PI; +} + +// Keep the Web Mercator world covering the whole viewport so no background gap +// (the "blue band") can ever show. Enforces a minimum zoom where the world spans +// the larger viewport dimension, then clamps the center latitude so the poles +// can't be panned into view. 2D only — GlobeView has no such gap. +function clampToWorld(vs: MapViewState, width: number, height: number): MapViewState { + if (!width || !height) return vs; + const minZoom = Math.log2(Math.max(width, height) / TILE_SIZE); + const zoom = Math.max(typeof vs.zoom === 'number' ? vs.zoom : 0, minZoom); + const worldPx = TILE_SIZE * Math.pow(2, zoom); + const halfFrac = Math.min(0.5, height / 2 / worldPx); + let y = latToMercY(typeof vs.latitude === 'number' ? vs.latitude : 0); + y = Math.min(1 - halfFrac, Math.max(halfFrac, y)); + return { ...vs, zoom, latitude: mercYToLat(y) }; +} + interface LogoMarker { company: Company; x: number; @@ -408,10 +434,13 @@ export function DeckMap({
{ - onViewStateChange(vs as MapViewState); + const next = is3D + ? (vs as MapViewState) + : clampToWorld(vs as MapViewState, size.w, size.h); + onViewStateChange(next); if ( interactionState && (interactionState.isDragging || interactionState.isPanning || interactionState.isZooming) diff --git a/frontend/src/components/WorldMap/WorldMap.tsx b/frontend/src/components/WorldMap/WorldMap.tsx index b266018..585bacc 100644 --- a/frontend/src/components/WorldMap/WorldMap.tsx +++ b/frontend/src/components/WorldMap/WorldMap.tsx @@ -433,8 +433,9 @@ export function WorldMap() {
import('../components/WorldMap')); +import { DatabasePreview } from '../components/DatabasePreview'; import { EmailSubscription } from '../components/EmailSubscription'; import { CompaniesBrowser } from '../components/CompaniesBrowser'; import { CompanyDetailModal } from '../components/CompanyDetailModal'; @@ -160,7 +160,7 @@ export function HomePage() { className="mt-8 flex flex-wrap items-center justify-center gap-3" > Start Exploring @@ -264,29 +264,32 @@ export function HomePage() { )} - {/* Map Section */} + {/* Database Preview Section */} -
- - $ -

Interactive World Map

+
+
+ + $ +

Companies Database

+
+ {/* The interactive map now lives on its own page */} + + + Explore the interactive world map + +
- - Loading world map… -
- } - > - - +
{/* Daily YC Updates */} @@ -332,7 +335,7 @@ export function HomePage() { viewport={{ once: true, margin: '-80px' }} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-6xl mx-auto" > - +

Interactive Map

@@ -427,9 +430,9 @@ export function HomePage() { Ready to explore?
- + $ view map - + $ see charts diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx new file mode 100644 index 0000000..9a167c1 --- /dev/null +++ b/frontend/src/pages/MapPage.tsx @@ -0,0 +1,55 @@ +import { lazy, Suspense } from 'react'; +import { motion } from 'framer-motion'; +import { Helmet } from 'react-helmet-async'; +import { PageHeader } from '../components/ui/PageHeader'; +import { DotPattern } from '../components/ui/dot-pattern'; +import { CompanyDetailModal } from '../components/CompanyDetailModal'; +import { useApp } from '../contexts/AppContext'; + +const WorldMap = lazy(() => import('../components/WorldMap')); + +export function MapPage() { + const { selectedCompany, setSelectedCompany } = useApp(); + + return ( +
+ + Interactive World Map | ExploreYC + + + + + +
+ + + + + + Loading world map… +
+ } + > + + +
+ + {selectedCompany && ( + setSelectedCompany(null)} + /> + )} +
+ ); +} From 604667dbf055ac97b7bdbfd88f9c1735fe0dceed Mon Sep 17 00:00:00 2001 From: KonstantinMB Date: Mon, 13 Jul 2026 13:43:44 +0300 Subject: [PATCH 2/2] =?UTF-8?q?feat(home):=20redesign=20lower=20homepage?= =?UTF-8?q?=20=E2=80=94=20capabilities=20grid=20+=20animated=20API=20showc?= =?UTF-8?q?ase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the CompaniesBrowser 'Browse All Companies' block, the bento feature grid, and the 'Ready to explore?' terminal footer with two designed sections: - PlatformCapabilities: a live stat strip (companies/active/hiring/top industry /country) + a 6-card anchor grid pointing to Database, Map, Analytics, Hiring, Founders and Tools, with staggered reveals and per-card accent glow. - ApiShowcase: a live terminal that types a real curl request and streams syntax-highlighted JSON back, cycling through /companies, /search and /stats (real api.exploreyc.com/api/v1 endpoints + Bearer auth). Paired with a 3-step connect guide, endpoint chips, and CTAs to /signup and /api-docs. Respects prefers-reduced-motion; terminal stays dark in both themes. Removes now-unused CompaniesBrowser/HackerCard-bento imports from HomePage. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01R4B1pcs7619oF8o4jn3PZF --- frontend/src/components/ApiShowcase.tsx | 386 ++++++++++++++++++ .../src/components/PlatformCapabilities.tsx | 170 ++++++++ frontend/src/pages/HomePage.tsx | 139 +------ 3 files changed, 571 insertions(+), 124 deletions(-) create mode 100644 frontend/src/components/ApiShowcase.tsx create mode 100644 frontend/src/components/PlatformCapabilities.tsx diff --git a/frontend/src/components/ApiShowcase.tsx b/frontend/src/components/ApiShowcase.tsx new file mode 100644 index 0000000..9d186e8 --- /dev/null +++ b/frontend/src/components/ApiShowcase.tsx @@ -0,0 +1,386 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { Terminal, KeyRound, ArrowRight, Copy, Check, Zap } from 'lucide-react'; + +// Real ExploreYC public API surface (mirrors /api-docs). +const API_BASE = 'https://api.exploreyc.com/api/v1'; + +interface ApiExample { + method: string; + path: string; + cmd: string; + response: string[]; +} + +const EXAMPLES: ApiExample[] = [ + { + method: 'GET', + path: '/companies', + cmd: `curl -H "Authorization: Bearer eyc_live_…" \\\n "${API_BASE}/companies?source=yc&limit=2"`, + response: [ + '{', + ' "companies": [', + ' { "id": 15231, "name": "Stripe",', + ' "one_liner": "Payments infrastructure for the internet.",', + ' "batch": "Summer 2009", "industry": "Fintech",', + ' "status": "Active", "is_hiring": true },', + ' { "id": 18442, "name": "Airbnb",', + ' "batch": "Winter 2009", "industry": "Consumer" }', + ' ],', + ' "total": 6049,', + ' "has_more": true', + '}', + ], + }, + { + method: 'GET', + path: '/search', + cmd: `curl -H "Authorization: Bearer eyc_live_…" \\\n "${API_BASE}/search?q=fintech&limit=2"`, + response: [ + '{', + ' "companies": [', + ' { "id": 15231, "name": "Stripe",', + ' "industry": "Fintech", "batch": "Summer 2009" },', + ' { "id": 20817, "name": "Brex",', + ' "industry": "Fintech", "batch": "Winter 2017" }', + ' ],', + ' "total": 214,', + ' "has_more": true', + '}', + ], + }, + { + method: 'GET', + path: '/stats', + cmd: `curl -H "Authorization: Bearer eyc_live_…" \\\n "${API_BASE}/stats"`, + response: [ + '{', + ' "total_companies": 6049,', + ' "hiring": 1530,', + ' "top_industry": "B2B",', + ' "top_country": "United States",', + ' "batches": 50', + '}', + ], + }, +]; + +const ENDPOINT_CHIPS = [ + '/companies', + '/search', + '/stats', + '/map', + '/batches', + '/industries', + '/countries', +]; + +function usePrefersReducedMotion(): boolean { + const [reduced, setReduced] = useState( + () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches + ); + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + const onChange = () => setReduced(mq.matches); + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, []); + return reduced; +} + +// Minimal JSON syntax highlighter → colored spans matching the platform palette. +function highlight(line: string): React.ReactNode[] { + const nodes: React.ReactNode[] = []; + const re = /("(?:[^"\\]|\\.)*")(\s*:)?|(-?\d+\.?\d*)|(true|false|null)|([{}[\],:])/g; + let last = 0; + let m: RegExpExecArray | null; + let key = 0; + while ((m = re.exec(line))) { + if (m.index > last) nodes.push(line.slice(last, m.index)); + if (m[1]) { + const isKey = !!m[2]; + nodes.push( + + {m[1]} + + ); + if (m[2]) nodes.push({m[2]}); + } else if (m[3]) { + nodes.push({m[3]}); + } else if (m[4]) { + nodes.push({m[4]}); + } else if (m[5]) { + nodes.push({m[5]}); + } + last = re.lastIndex; + } + if (last < line.length) nodes.push(line.slice(last)); + return nodes; +} + +type Phase = 'typing' | 'fetching' | 'responding' | 'hold'; + +export function ApiShowcase() { + const reduced = usePrefersReducedMotion(); + const [idx, setIdx] = useState(0); + const [typed, setTyped] = useState(0); + const [lines, setLines] = useState(0); + const [phase, setPhase] = useState('typing'); + const [copied, setCopied] = useState(false); + const timers = useRef([]); + + const ex = EXAMPLES[idx]; + + // Drive the typewriter → fetch → stream-response → hold state machine. + useEffect(() => { + if (reduced) { + // Skip animation: show the full request + response, cycle slowly. + setTyped(ex.cmd.length); + setLines(ex.response.length); + const t = window.setTimeout(() => setIdx((i) => (i + 1) % EXAMPLES.length), 6000); + return () => window.clearTimeout(t); + } + + let cancelled = false; + const clearAll = () => timers.current.forEach((t) => window.clearTimeout(t)); + + if (phase === 'typing') { + if (typed < ex.cmd.length) { + const t = window.setTimeout(() => !cancelled && setTyped((n) => n + 1), 16); + timers.current.push(t); + } else { + const t = window.setTimeout(() => !cancelled && setPhase('fetching'), 450); + timers.current.push(t); + } + } else if (phase === 'fetching') { + const t = window.setTimeout(() => !cancelled && setPhase('responding'), 650); + timers.current.push(t); + } else if (phase === 'responding') { + if (lines < ex.response.length) { + const t = window.setTimeout(() => !cancelled && setLines((n) => n + 1), 80); + timers.current.push(t); + } else { + const t = window.setTimeout(() => !cancelled && setPhase('hold'), 2800); + timers.current.push(t); + } + } else if (phase === 'hold') { + const t = window.setTimeout(() => { + if (cancelled) return; + setTyped(0); + setLines(0); + setPhase('typing'); + setIdx((i) => (i + 1) % EXAMPLES.length); + }, 200); + timers.current.push(t); + } + + return () => { + cancelled = true; + clearAll(); + }; + }, [phase, typed, lines, idx, reduced, ex.cmd.length, ex.response.length]); + + const typedCmd = ex.cmd.slice(0, typed); + const showResponse = phase === 'responding' || phase === 'hold' || reduced; + const fetching = phase === 'fetching'; + + const copyCmd = async () => { + try { + await navigator.clipboard.writeText(ex.cmd.replace('eyc_live_…', 'eyc_live_your_key')); + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } catch { + /* clipboard blocked — no-op */ + } + }; + + const steps = useMemo( + () => [ + { + n: '01', + title: 'Grab a key', + body: 'Create a free developer account and generate an API key from the dashboard.', + code: 'eyc_live_••••••••', + to: '/signup', + cta: 'Get a key', + }, + { + n: '02', + title: 'Send a request', + body: 'Pass your key as a bearer token. Filter by batch, industry, country, hiring & more.', + code: 'Authorization: Bearer eyc_live_…', + to: '/api-docs', + cta: 'See params', + }, + { + n: '03', + title: 'Get YC data', + body: 'Clean JSON across YC + a16z — companies, search, stats, maps, batch wrapped.', + code: '200 OK · application/json', + to: '/database', + cta: 'Browse data', + }, + ], + [] + ); + + return ( +
+ {/* Section heading */} +
+
+
+ + $ curl api.exploreyc.com/api/v1 +
+

+ > YC data, one request away +

+

+ A public REST API over every company we track — YC & a16z. Authenticate, query, + and get structured JSON back. No scraping. +

+
+
+ + + Get your API key + + + + Read the docs + +
+
+ +
+ {/* Terminal window — always dark so it reads as a real console in both themes */} + + {/* window chrome */} +
+ + + + + exploreyc — api — curl + +
+ + {ex.method} {ex.path} + + +
+
+ + {/* terminal body */} +
+ {/* request */} +
+ $ + {typedCmd} + {phase === 'typing' && !reduced && ( + + )} +
+ + {/* fetching spinner */} + {fetching && ( +
+ + fetching… +
+ )} + + {/* response */} + {showResponse && ( +
+
↳ 200 OK · application/json
+
+                  {ex.response.slice(0, reduced ? ex.response.length : lines).map((ln, i) => (
+                    
+                      {highlight(ln)}
+                    
+                  ))}
+                
+
+ )} +
+ + {/* endpoint chips */} +
+ {ENDPOINT_CHIPS.map((chip) => ( + + {chip} + + ))} +
+
+ + {/* Connect steps */} +
+ {steps.map((s, i) => ( + +
+ + {s.n} + +
+

{s.title}

+

{s.body}

+ + {s.code} + + + {s.cta} + +
+
+
+ ))} +
+ + Free tier available · rate-limited · no credit card +
+
+
+
+ ); +} diff --git a/frontend/src/components/PlatformCapabilities.tsx b/frontend/src/components/PlatformCapabilities.tsx new file mode 100644 index 0000000..04fbf53 --- /dev/null +++ b/frontend/src/components/PlatformCapabilities.tsx @@ -0,0 +1,170 @@ +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { + Database, Globe2, BarChart3, Briefcase, BookOpen, Wrench, ArrowRight, Sparkles, +} from 'lucide-react'; +import { useApp } from '../contexts/AppContext'; + +interface Capability { + cmd: string; + title: string; + desc: string; + to: string; + icon: React.ComponentType<{ className?: string }>; + accent: string; // text/border accent + glow: string; // hover shadow +} + +const CAPABILITIES: Capability[] = [ + { + cmd: '$ db --companies', + title: 'Companies Database', + desc: 'Every YC & a16z company in one sortable, filterable table — batch, industry, funding, team.', + to: '/database', + icon: Database, + accent: 'text-[#FB651E] group-hover:border-[#FB651E]/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(251,101,30,0.15)]', + }, + { + cmd: '$ map --world', + title: 'Interactive World Map', + desc: 'Watch YC spread across the globe — 2D map, 3D globe, density hotspots, batch timeline.', + to: '/map', + icon: Globe2, + accent: 'text-sky-400 group-hover:border-sky-400/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(56,189,248,0.15)]', + }, + { + cmd: '$ analytics', + title: 'Analytics & Charts', + desc: 'Batches, industries, geography and hiring trends — the shape of Y Combinator over time.', + to: '/analytics', + icon: BarChart3, + accent: 'text-blue-400 group-hover:border-blue-400/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(96,165,250,0.15)]', + }, + { + cmd: '$ jobs --hiring', + title: 'Hiring Board', + desc: 'Open roles across every company that is actively hiring, refreshed daily.', + to: '/hiring', + icon: Briefcase, + accent: 'text-emerald-400 group-hover:border-emerald-400/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(52,211,153,0.15)]', + }, + { + cmd: '$ founders', + title: 'For Founders', + desc: 'PG essays, daily YC updates, and the playbook — everything a founder actually reads.', + to: '/founders', + icon: BookOpen, + accent: 'text-violet-400 group-hover:border-violet-400/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(167,139,250,0.15)]', + }, + { + cmd: '$ tools --launch', + title: 'Founder Tools', + desc: 'Idea validator, success predictor, batch wrapped and a fundraising tracker.', + to: '/tools', + icon: Wrench, + accent: 'text-amber-400 group-hover:border-amber-400/50', + glow: 'group-hover:shadow-[0_0_24px_rgba(251,191,36,0.15)]', + }, +]; + +export function PlatformCapabilities() { + const { stats } = useApp(); + + const total = (stats?.total_all_companies ?? stats?.total_companies)?.toLocaleString() ?? '—'; + const active = stats?.by_status?.['Active']?.toLocaleString() ?? '—'; + const hiring = stats?.hiring?.toLocaleString() ?? '—'; + const topIndustry = + stats?.by_industry + ? Object.entries(stats.by_industry).sort(([, a], [, b]) => b - a)[0]?.[0] + : undefined; + const topCountry = + stats?.by_country + ? Object.entries(stats.by_country).sort(([, a], [, b]) => b - a)[0]?.[0] + : undefined; + + const statStrip = [ + { label: 'companies tracked', value: total }, + { label: 'active', value: active }, + { label: 'hiring now', value: hiring }, + { label: 'top industry', value: topIndustry ?? '—' }, + { label: 'top country', value: topCountry ?? '—' }, + ]; + + return ( +
+ {/* Heading */} +
+
+ + $ explore --all +
+

+ > Everything you can explore +

+
+ + {/* Live stat strip */} + + {statStrip.map((s) => ( +
+
{s.value}
+
+ {s.label} +
+
+ ))} +
+ + {/* Capability grid */} +
+ {CAPABILITIES.map((c, i) => { + const Icon = c.icon; + return ( + + +
+
+
+ +
+ {c.cmd} +
+

{c.title}

+

{c.desc}

+ + open + + {/* corner sweep on hover */} +
+
+ + + ); + })} +
+
+ ); +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 1eeeadf..91d4b6f 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -5,11 +5,12 @@ import { useApp } from '../contexts/AppContext'; import { apiClient, type Source } from '../lib/api'; import { SourceBadge } from '../components/ui/SourceBadge'; import { getSecondMostRecentBatch, batchToShortFormat as batchToShort } from '../lib/batchUtils'; -import { Map, BarChart3, Wrench, TrendingUp, Building2, Globe2, Sparkles, ArrowRight, BookOpen, Terminal, ChevronUp } from 'lucide-react'; +import { Globe2, Sparkles, ArrowRight, Terminal, ChevronUp } from 'lucide-react'; import { HeroAnswerBox } from '../components/HeroAnswerBox'; import { DatabasePreview } from '../components/DatabasePreview'; +import { PlatformCapabilities } from '../components/PlatformCapabilities'; +import { ApiShowcase } from '../components/ApiShowcase'; import { EmailSubscription } from '../components/EmailSubscription'; -import { CompaniesBrowser } from '../components/CompaniesBrowser'; import { CompanyDetailModal } from '../components/CompanyDetailModal'; import { HackerCard } from '../components/ui/hacker-card'; import { DotPattern } from '../components/ui/dot-pattern'; @@ -303,20 +304,15 @@ export function HomePage() { - {/* Companies List */} + {/* Platform capabilities — anchors into every data surface */} -
- $ -

Browse All Companies

- ({(stats?.total_companies ?? 0).toLocaleString()} startups) -
- +
{selectedCompany && ( @@ -327,121 +323,16 @@ export function HomePage() { /> )} - {/* Bento-style feature grid */} - - - - -

Interactive Map

-

- Explore companies by location on a global map -

- - view - -
- - - - - -

Analytics

-

- Batches, industries, hiring trends -

- - open - -
- - - - - -

For Founders

-

- PG essays, daily YC updates -

- - read - -
- - - - - -

Founder Tools

-

- Idea validator, batch wrapped, fundraising tracker -

- - launch - -
- - - {/* Stats cards - compact */} - {stats?.by_status && ( - - -
- {stats.by_status['Active']?.toLocaleString() || 0} -
-
Active
-
- )} - {stats?.by_industry && ( - - -
- {Object.entries(stats.by_industry).sort(([, a], [, b]) => b - a)[0]?.[0] || 'N/A'} -
-
Top Industry
-
- )} - {stats?.by_country && ( - - -
- {Object.entries(stats.by_country).sort(([, a], [, b]) => b - a)[0]?.[0] || 'N/A'} -
-
Top Country
-
- )} -
- - {/* Terminal footer */} - -
-
- > - Ready to explore? -
-
- - $ view map - - - $ see charts - - - $ validate idea - -
-
-
+ +
);