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
3 changes: 3 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ LAUNCH_SYNC_CONFIRMATIONS=2
# Token logo uploads: "local" stores files in .uploads/ (dev). In production set the Tigris/S3 secrets
# (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION, BUCKET_NAME) instead.
IMAGE_STORE=local
# The hero locker's coin wears a new launch's own picture (our images only, never a copy, never a look-alike name). Set to "off"
# to keep the coin plain (the caption still reacts). On Fly: `fly secrets set HERO_COIN_PICTURES=off` restarts the app, no deploy.
# HERO_COIN_PICTURES=off

# Wallets allowed to moderate posts (comma-separated, lowercase)
ADMIN_WALLETS=
112 changes: 88 additions & 24 deletions app/src/app/(home)/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,99 @@
import { Sk, SkPost, SkRow } from "@/components/Skeleton";
import { HERO } from "@/components/launchpad/hero-layout";
import hero from "@/components/launchpad/LaunchHero.module.css";
import machine from "@/components/launchpad/LaunchMachine.module.css";
import totals from "@/components/launchpad/LaunchMechanism.module.css";
import trend from "@/components/launchpad/TrendingStrip.module.css";

/**
* Home skeleton, in the page's own order: hero (text, the locker in its stage and the totals, in the real grid), the live
* line, Trending board, live panel, then the launches list with its side column. The hero's class strings come from
* hero-layout.ts and its grid, stage, locker and totals from the real components' stylesheets, so heights follow the
* real layout (including the short-screen compaction) instead of copying it. Only the Trending and live-panel blocks
* are sized by hand.
*/
/** One compact board card (a runner, or the leader below 1024px) at the heights the real card has. */
function SkBoardCard() {
return (
<div className="flex h-[138px] flex-col justify-between rounded-2xl border border-line bg-card px-4 py-3.5 sm:h-[144px] sm:py-4 lg:h-[156px]">
<div className="flex items-center gap-2.5"><Sk className="h-[38px] w-[38px] shrink-0 rounded-xl" /><div className="min-w-0 flex-1 space-y-2"><Sk className="h-3.5 w-2/3" /><Sk className="h-2.5 w-20" /></div><Sk className="h-3.5 w-14" /></div>
<div className="space-y-2.5"><Sk className="h-[3px] w-full rounded-full" /><Sk className="h-2.5 w-3/4" /></div>
</div>
);
}

/** Home skeleton: hero + totals, list header, rows, side column. Same grid as page.tsx so nothing shifts. */
export default function Loading() {
return (
<main className="bb-mid bb-page relative pb-16 space-y-8" aria-busy="true" aria-label="loading">
<section className="pt-10 sm:pt-14 pb-2 grid lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] gap-x-8 gap-y-8 xl:gap-x-12 lg:gap-y-0 lg:grid-rows-[min-content_1fr]">
<div className="min-w-0 lg:col-start-1 lg:row-start-1">
<div className="space-y-2">
{[0, 1, 2].map((i) => <Sk key={i} className="h-11 sm:h-14 lg:h-12 xl:h-14 w-11/12" />)}
<main className="bb-mid bb-page relative space-y-6 pb-16 sm:space-y-8" aria-busy="true" aria-label="loading">
<section className={`${HERO.section} ${hero.hero}`}>
<div className={hero.layout}>
<div className={hero.copyArea}>
<span className="hidden h-7 w-80 max-w-full items-center sm:flex"><Sk className="h-7 w-full rounded-full" /></span>
{/* the headline's own classes: each bar sits in one line of its line-height, so two bars are as tall as the two lines */}
<div className={HERO.headline}>{[0, 1].map((i) => <span key={i} className="flex h-[1.02em] items-center"><Sk className={`h-[0.74em] ${i ? "w-11/12" : "w-10/12"}`} /></span>)}</div>
{/* the copy's real wrap, in lines: 6 up to 380px, 5 up to 460px, then 4; beside the locker 6 at 768-790px, 5 to 950px, 4 to 1023px,
5 again at 1024-1069px, then 4. The last placeholder is the short final line. */}
<div className={HERO.copy}>
{[
{ show: "flex" },
{ show: "flex" },
{ show: "flex" },
{ show: "hidden max-[459px]:flex min-[768px]:max-[949px]:flex min-[1024px]:max-[1069px]:flex" },
{ show: "hidden max-[380px]:flex min-[768px]:max-[790px]:flex" },
{ show: "flex", width: "w-2/3" },
].map((line, i) => <span key={i} className={`h-lh items-center ${line.show}`}><Sk className={`h-[0.7em] ${line.width ?? "w-full"}`} /></span>)}
</div>
<div className={HERO.actions}><Sk className="h-12 min-w-0 flex-1 rounded-2xl sm:h-[54px] sm:w-52 sm:flex-none" /><Sk className="h-12 w-[149px] shrink-0 rounded-2xl max-[379px]:w-12 sm:h-[54px] sm:w-56" /></div>
<div className={HERO.proofs}>{["w-[106px] sm:w-[122px]", "w-[96px] sm:w-[170px]", "w-[133px] sm:w-[152px]"].map((w) => <Sk key={w} className={`h-8 rounded-full sm:h-9 ${w}`} />)}</div>
</div>
<div className="mt-6 space-y-2">{[0, 1, 2].map((i) => <Sk key={i} className="h-5 w-11/12" />)}</div>
<div className="mt-8 flex flex-col sm:flex-row gap-3 sm:gap-5"><Sk className="h-12 w-full sm:w-48 rounded-xl" /><Sk className="h-5 w-28 self-center" /></div>
<Sk className="mt-6 h-4 w-64 max-w-full" />
</div>
<div className="min-w-0 lg:col-start-2 lg:row-start-1 lg:row-span-2 self-center">
<div className="mx-auto max-w-[560px]">
<div className="flex h-9 items-center"><Sk className="h-3 w-48" /></div>
<div className="aspect-[560/398] flex items-center justify-center"><Sk className="h-2/3 w-3/4 rounded-2xl" /></div>
<div className="grid grid-cols-3 gap-4 border-b border-line py-4">{[0, 1, 2].map((i) => <Sk key={i} className="h-3 w-20 max-w-full" />)}</div>
<div className="h-[105px] space-y-3 pt-4"><Sk className="h-4 w-56 max-w-full" /><Sk className="h-3 w-full" /><Sk className="h-3 w-4/5" /></div>
<Sk className="h-4 w-4/5" />
<div className={hero.stage}>
<div className={machine.machine}>
<div className="flex h-9 items-center"><Sk className="h-3 w-44" /></div>
<div className="flex aspect-[560/398] items-center justify-center"><Sk className="h-2/3 w-3/4 rounded-2xl" /></div>
<div className="grid grid-cols-3 border-b border-line">{[0, 1, 2].map((i) => <span key={i} className="flex h-11 items-center px-2"><Sk className="h-3 w-20 max-w-full" /></span>)}</div>
<div className={machine.detail}><div className="space-y-2.5"><Sk className="h-4 w-56 max-w-full" /><Sk className="h-3 w-full max-w-[390px]" /><Sk className="h-3 w-3/5" /></div></div>
{/* the locker's proof links drop to a second row on the narrowest phones */}
<div className="flex h-[53px] items-center px-2 min-[340px]:h-[30px]"><Sk className="h-2.5 w-full" /></div>
</div>
</div>
<div className={hero.totalsArea}>
<div className={totals.panel}>
<div className={totals.readings}>
{[0, 1, 2, 3, 4].map((i) => (
<div key={i} className={i ? totals.reading : `${totals.reading} ${totals.lead}`}>
<div className={`${totals.label} flex min-h-[1.4em] items-center max-[359px]:min-h-[2.8em] lg:max-[1179px]:min-h-[2.8em]`}><Sk className="h-2.5 w-20" /></div>
<div className={`${totals.figure} flex items-center justify-end sm:justify-start`}><Sk className={i ? "h-4 w-16 sm:h-5" : "h-6 w-32 sm:h-8 sm:w-44 lg:h-10 lg:w-36"} /></div>
<div className={`${totals.note} ${i ? "min-h-[3em]" : "min-h-[1.5em]"}`}><Sk className="mt-[0.4em] h-[0.8em] w-4/5" /></div>
</div>
))}
</div>
<div className={`${totals.breakdown} flex min-h-11 items-center lg:min-h-9`}><Sk className="h-3 w-64 max-w-full" /></div>
</div>
</div>
</div>
<div className="min-w-0 lg:col-start-1 lg:row-start-2 lg:mt-9 border-t border-line pt-4">
<Sk className="h-4 w-full" />
<div className="mt-5 grid grid-cols-3 gap-4">{[0, 1, 2].map((i) => <div key={i} className="space-y-3"><Sk className="h-3 w-24 max-w-full" /><Sk className="h-7 w-24 max-w-full" /></div>)}</div>
<Sk className="mt-5 h-3 w-44" />
</div>
<div className={HERO.ticker}><Sk className="h-10 w-full rounded-xl" /></div>
</section>
{/* Trending (TrendingStrip.tsx): a row to swipe below 1024px, a leader over four runners from there. The row's own stylesheet
class is used, so its padding, gap and card width are the real ones. */}
<section className="min-w-0">
<div className="mb-3 flex h-8 items-center gap-3"><Sk className="h-4 w-20" /><Sk className="h-5 w-24 rounded-full" /></div>
<ol className={`${trend.board} lg:grid-cols-10 lg:grid-rows-2`}>
<li className="lg:col-span-4 lg:row-span-2">
<div className="lg:hidden"><SkBoardCard /></div>
<div className="hidden h-[324px] flex-col justify-between rounded-2xl border border-line-strong bg-card p-5 lg:flex">
<div className="space-y-4"><Sk className="h-3 w-40" /><div className="flex items-center gap-3.5"><Sk className="h-[52px] w-[52px] shrink-0 rounded-2xl" /><div className="min-w-0 flex-1 space-y-2"><Sk className="h-4 w-2/3" /><Sk className="h-3 w-20" /></div></div></div>
<div className="space-y-3"><Sk className="h-4 w-36" /><Sk className="h-1.5 w-full rounded-full" /><Sk className="h-3 w-3/4" /></div>
</div>
</li>
{[0, 1, 2, 3].map((i) => <li key={i} className="lg:col-span-3"><SkBoardCard /></li>)}
</ol>
</section>
{/* the live panel (LiveRiver.tsx): a rounded card, 510px on phones, 588px on tablets and 463px on desktop with its bubbles view open */}
<section className="relative h-[510px] rounded-3xl border border-line bg-card/70 p-4 shadow-card sm:h-[588px] sm:p-6 lg:h-[463px]">
<div className="flex h-8 items-center justify-between gap-4"><Sk className="h-4 w-56 max-w-[60%]" /><Sk className="h-8 w-40 rounded-lg" /></div>
<Sk className="mt-4 h-[calc(100%-3.5rem)] w-full rounded-2xl" />
</section>
<div className="space-y-3"><Sk className="h-4 w-40" /><Sk className="h-16 w-full rounded-xl" /></div>
<div className="grid xl:grid-cols-[minmax(0,1fr)_17rem] gap-6 items-start">
<div className="grid items-start gap-x-8 gap-y-6 xl:grid-cols-[minmax(0,1fr)_19rem]">
<section className="overflow-hidden rounded-2xl border border-line bg-paper">
<div className="flex flex-wrap items-center justify-between gap-3 px-4 pt-5">
<Sk className="h-7 w-32" />
Expand Down
32 changes: 15 additions & 17 deletions app/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import LiveRiver from "@/components/launchpad/LiveRiver";
import JustLaunched from "@/components/launchpad/JustLaunched";
import { PostsFeed } from "@/components/launchpad/Posts";
import { listFeed } from "@/lib/launchpad/postsServer";
import { VOLUME_WINDOWS, getLaunchFeed, getTrending, isTrendingSource, listLaunchesPage, parseSort, trendingFrom, type VolumeWindow } from "@/lib/launchpad/queries";
import { VOLUME_WINDOWS, getLaunchFeed, getTrending, isTrendingSource, listLaunchesPage, parseSort, trendingFrom, withBoardTape, type VolumeWindow } from "@/lib/launchpad/queries";
import { JUST_LAUNCHED_SIZE, PAGE_SIZE } from "@/lib/launchpad/paging";
import { ethUsd } from "@/lib/launchpad/ethPrice";
import { LAUNCHPAD_CONFIGURED, visibleChainOr } from "@/lib/launchpad/config";
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 { feedKey, inRiverWindow, riverCoverage } from "@/lib/launchpad/river";
import TrendingStrip from "@/components/launchpad/TrendingStrip";
import LaunchPill from "@/components/launchpad/LaunchPill";
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";

Expand All @@ -43,21 +43,16 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
listFeed(30),
isTrendingSource(listOpts) ? null : getTrending(usd),
]);
const trending = fetchedTrending ?? trendingFrom(page.items); // the default view's first page doubles as the strip's candidates
const trending = await withBoardTape(fetchedTrending ?? trendingFrom(page.items)); // the default view's first page doubles as the strip's candidates; the tapes are the cards' recent trades
const now = nowMs();

return (
<main className="bb-mid relative pb-16">
{/* the hero and the river share one full-bleed band; it clips its own backdrop so nothing widens the page */}
<div className="relative isolate overflow-hidden">
<HeroBackdrop />
<div className="bb-page space-y-8 pb-4">
<LaunchHero configured={LAUNCHPAD_CONFIGURED} />
<LiveRiver initial={feed.filter((item) => inRiverWindow(item, now))} recent={feed} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} />
</div>
</div>
<div className="bb-page mt-6 space-y-8">
<TrendingStrip initial={trending} />
<>
<main className="bb-mid bb-page relative space-y-6 pb-16 sm:space-y-8">
<LaunchHero configured={LAUNCHPAD_CONFIGURED} />
{/* what is hot first (names and numbers to act on), then the last half hour as it happens, then every launch */}
<TrendingStrip initial={trending} serverNow={now} seenKeys={feed.filter((item) => item.kind === "swap").map(feedKey)} />
<LiveRiver initial={feed.filter((item) => inRiverWindow(item, now))} recent={feed} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} />
<div className="grid items-start gap-x-8 gap-y-6 xl:grid-cols-[minmax(0,1fr)_19rem]">
<MagicCard className="min-w-0 rounded-2xl"><LaunchList frame="rounded-[inherit]" initial={page.items} initialHasMore={page.hasMore} initialSort={sort} initialWindow={window} initialChain={chain} initialFilter={filter} initialView={sp.view === "watchlist" ? "watchlist" : "market"} initialLayout={layout} hasDb={dbConfigured()} serverNow={now} /></MagicCard>
<aside aria-label="New launches and community" className="grid min-w-0 gap-4 md:grid-cols-2 xl:sticky xl:top-24 xl:grid-cols-1">
Expand All @@ -68,7 +63,10 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
</div>
</aside>
</div>
</div>
</main>
</main>
{/* outside <main>: it is fixed to the screen, and as <main>'s last child it switched the spacing of the grid above it
(space-y skips only the last child) every time it came or went, moving the page's height and the footer with it */}
<LaunchPill />
</>
);
}
2 changes: 2 additions & 0 deletions app/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.bb-beacon::after { content: ""; position: absolute; inset: 0; border-radius: 9999px; background: var(--color-up); opacity: 0.55; animation: bb-beacon 2s cubic-bezier(0.2, 0.7, 0.3, 1) infinite; }
@keyframes bb-beacon { 0% { transform: scale(1); opacity: 0.55; } 70% { transform: scale(2.8); opacity: 0; } 100% { transform: scale(2.8); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .bb-beacon::after { animation: none; opacity: 0; } }
/* a container marked by usePauseOffscreen holds all of its animations while it is off screen, and resumes them where they were */
[data-offscreen="true"], [data-offscreen="true"] *, [data-offscreen="true"] *::before, [data-offscreen="true"] *::after { animation-play-state: paused !important; }

/* One finite entry reaction for a real activity event. */
.bb-tape-enter { animation: bb-tape-enter 240ms ease-out both; }
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/rules/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function RulesPage() {
<div><p className={styles.eyebrow}><Rocket size={14} aria-hidden="true" />The launch</p><h2 id="launch-heading">One signature.<br />Four things happen.</h2></div>
<p>All in the same transaction. <br />No separate setup. No platform fee.</p>
</div>
{/* the animated walk-through that used to open the home page: token, pool, lock */}
{/* the animated walk-through that also opens the home page: token, pool, lock */}
<div className={styles.machine}><LaunchMachine /></div>
<ol className={styles.steps}>
{STEPS.map(({ title, description, value, label }, i) => (
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/launchpad/ChangeChip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { fmtCompact } from "@/lib/launchpad/math";
import { marketChange } from "@/lib/launchpad/market-format";
import Roll from "./Roll";

/** "+12%" / "-3.4%" / "19.7×" since launch. Plain component: renders on the server and the client. */
export default function ChangeChip({ v, className = "", plain = false, context = "since launch" }: { v: number; className?: string; plain?: boolean; context?: string }) {
export default function ChangeChip({ v, className = "", plain = false, roll = false, context = "since launch" }: { v: number; className?: string; plain?: boolean; roll?: boolean; context?: string }) {
const pct = v * 100;
const available = Number.isFinite(pct);
const up = pct >= 0;
Expand All @@ -14,7 +15,7 @@ export default function ChangeChip({ v, className = "", plain = false, context =
const pillColor = !available || flat ? "bg-paper text-muted" : up ? "bg-up-soft text-up" : "bg-down-soft text-down-ink";
return (
<span className={`inline-flex max-w-full items-center text-[11px] font-mono font-bold tnum ${plain ? (market.direction === "flat" ? "text-muted" : market.direction === "up" ? "text-up" : "text-down-ink") : `rounded-md px-1.5 h-5 ${pillColor}`} ${className}`} title={`${available ? `${pct}%` : "—"} ${context}`}>
<span className="truncate">{plain ? market.label : txt}</span>
<span className="truncate">{roll ? <Roll text={plain ? market.label : txt} /> : plain ? market.label : txt}</span>
</span>
);
}
14 changes: 0 additions & 14 deletions app/src/components/launchpad/HeroBackdrop.tsx

This file was deleted.

Loading
Loading