diff --git a/app/package-lock.json b/app/package-lock.json index f6ee7dd..32a0401 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -14,6 +14,7 @@ "@number-flow/react": "^0.6.2", "@tanstack/react-query": "^5.91.3", "clsx": "^2.1.1", + "d3-force": "^3.0.0", "lightweight-charts": "^5.2.1", "lucide-react": "^1.42.0", "motion": "^13.2.0", @@ -30,6 +31,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-force": "^3.0.10", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -2808,6 +2810,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -4018,6 +4027,47 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/app/package.json b/app/package.json index fc1afe1..c253c11 100644 --- a/app/package.json +++ b/app/package.json @@ -18,6 +18,7 @@ "@number-flow/react": "^0.6.2", "@tanstack/react-query": "^5.91.3", "clsx": "^2.1.1", + "d3-force": "^3.0.0", "lightweight-charts": "^5.2.1", "lucide-react": "^1.42.0", "motion": "^13.2.0", @@ -34,6 +35,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-force": "^3.0.10", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/app/src/app/(home)/loading.tsx b/app/src/app/(home)/loading.tsx index 9d91e2e..1dc8a34 100644 --- a/app/src/app/(home)/loading.tsx +++ b/app/src/app/(home)/loading.tsx @@ -3,7 +3,7 @@ import { Sk, SkPost, SkRow } from "@/components/Skeleton"; /** Home skeleton: hero + totals, list header, rows, side column. Same grid as page.tsx so nothing shifts. */ export default function Loading() { return ( -
+
diff --git a/app/src/app/(home)/page.tsx b/app/src/app/(home)/page.tsx index 42c563c..a1e9ba6 100644 --- a/app/src/app/(home)/page.tsx +++ b/app/src/app/(home)/page.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { cookies } from "next/headers"; import LaunchHero from "@/components/launchpad/LaunchHero"; import LaunchList from "@/components/launchpad/LaunchList"; @@ -15,8 +14,11 @@ import { dbConfigured } from "@/lib/db"; import { isFilter } from "@/lib/launchpad/search"; import { nowMs } from "@/lib/launchpad/time"; import { inRiverWindow, riverCoverage } from "@/lib/launchpad/river"; -import { LAYOUT_COOKIE, parseLayout } from "@/lib/launchpad/list-layout"; import TrendingStrip from "@/components/launchpad/TrendingStrip"; +import WhyFree from "@/components/launchpad/WhyFree"; +import HeroBackdrop from "@/components/launchpad/HeroBackdrop"; +import { MagicCard } from "@/components/vendor/magic-card"; +import { LAYOUT_COOKIE, parseLayout } from "@/lib/launchpad/list-layout"; export const dynamic = "force-dynamic"; // Title and description come from the root layout; ?sort= / ?chain= / ?window= / ?view= are all this one page. @@ -32,6 +34,7 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s const chain = visibleChainOr(sp.chain); const filter = isFilter(sp.filter) ? sp.filter : null; const usd = await ethUsd(); + const layout = parseLayout((await cookies()).get(LAYOUT_COOKIE)?.value); const listOpts = { sort, window, chain, filter, limit: PAGE_SIZE, ethUsd: usd }; const [page, feed, fresh, posts, fetchedTrending] = await Promise.all([ listLaunchesPage(listOpts), @@ -42,32 +45,29 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s ]); const trending = fetchedTrending ?? trendingFrom(page.items); // the default view's first page doubles as the strip's candidates const now = nowMs(); - const layout = parseLayout((await cookies()).get(LAYOUT_COOKIE)?.value); return ( -
- - inRiverWindow(item, now))} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} /> - -
- - +
+ {/* the hero and the river share one full-bleed band; it clips its own backdrop so nothing widens the page */} +
+ +
+ + inRiverWindow(item, now))} recent={feed} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} /> +
+
+
+ +
+ + +
); diff --git a/app/src/components/launchpad/HeroBackdrop.tsx b/app/src/components/launchpad/HeroBackdrop.tsx new file mode 100644 index 0000000..cd9a7e2 --- /dev/null +++ b/app/src/components/launchpad/HeroBackdrop.tsx @@ -0,0 +1,14 @@ +import { FlickeringGrid } from "@/components/vendor/flickering-grid"; + +/** + * Behind the home hero and the river: a flickering grid (grey on light, blue on dark) fading out from the top, with no colour wash + * over it. Decorative only; it sits in a clipped band so it never widens the page. + */ +export default function HeroBackdrop() { + return ( +