--- import Base from '../../layouts/Base.astro'; import ColorBends from '../../components/ColorBends.astro'; import MintCard from '../../components/MintCard.astro'; import PulseTicker from '../../components/PulseTicker.astro'; import { fetchFedimints, fetchHealth, fetchMint, fetchMints, fetchStats } from '../../lib/api'; import { fetchLatestReviews } from '../../lib/nostr-build'; import { feedTargets, targetPath } from '../../lib/feed-resolve'; import { displayDomain, iconGradient, sentiment, shortNpub } from '../../lib/format'; import { profileName, readCapabilities } from '@cashumints/shared'; import { localePath, useI18n, type Locale } from '../../i18n'; import { formatters } from '../../i18n/format'; import { localePaths } from '../../i18n/paths'; /** One page per locale: `/`, `/es`, `/nl`. */ export const getStaticPaths = localePaths; interface Props { locale: Locale; } const { locale } = Astro.props; const t = useI18n(locale); const f = formatters(t); const [mints, federations, stats, health] = await Promise.all([ fetchMints(), fetchFedimints(), fetchStats(), fetchHealth(), ]); const top = mints.slice(0, 6); /* * The reviews strip resolves against both ecosystems, because its heading is "latest * reviews" and not "latest mint reviews". Federations rarely reach it in practice — the * junk filter wants a written body and nearly every 38173 review is a bare `[5/5]` — * but when one does, it is genuinely among the latest and it links to its own page. */ const reviewTargets = feedTargets(mints, federations); /* * The mint details and the relay read have nothing to say to each other — the reviews * only need the target list above — so they wait together rather than one behind the * other. * * The carousel holds a screenful and a bit: enough that scrolling it is worth doing, * few enough that they are all real, recent, written reviews. Anything the relays * cannot fill is simply fewer cards, and the carousel's controls disappear with them. */ const [details, latest] = await Promise.all([ // Distributions give the card sentiment bars real data instead of a rating proxy. Promise.all(top.map((m) => fetchMint(m.host).catch(() => null))), fetchLatestReviews(reviewTargets, 10), ]); const sentiments = details.map((d) => (d ? sentiment(d.rating_distribution) : undefined)); // Same details, read a second way: the card's "melt only" or "frozen" chip. const capabilities = details.map((d) => (d ? readCapabilities(d.info?.nuts) : null)); const description = t('home.description', { mints: stats.mints_total, reviews: stats.reviews_total, }); /* * The one sentence on the page with a link inside it. Interpolated as markup rather * than split into three strings, so a translator can put "NIP-87" wherever the * sentence needs it instead of wherever English left a gap. */ const signedBody = t('home.why.signed.body', { link: `${t('home.why.signed.link')}`, }); --- {/* The hero sits on an animated colour field: ColorBends from React Bits, ported to plain WebGL in scripts/color-bends.ts. The wrapper is what the field fills, and it is full width, so the wash runs edge to edge rather than stopping at the 1240px column the text keeps to. */}
{/* The hero arrives as a sequence, 60ms apart, starting the moment the page is parsed. `data-reveal="load"` means exactly once, on a full page load: coming back here through the router puts it on screen without replaying the greeting. */}
{/* Two strings, because the emphasised half is a phrase and not the tail of a sentence: Spanish and Dutch both put "you can trust" somewhere English does not, and a translator needs to be able to move it. */}

{t('home.hero.title')} {t('home.hero.titleEm')}

{t('home.hero.lede')}

{t('home.top.title')}

{t('home.top.sub')} {t('home.top.all', { n: mints.length })}
{top.map((mint, i) => ( ))}
{/* The Fedimint strip. One row, under the mint grid and above the reviews, because that is what it is: a signpost to a second, smaller index, not a second front page. The home page stays Cashu-first — the hero, the search, the six ranked cards and the ticker are all unchanged — and this is the one place that says the other ecosystem exists. The counts come from `/api/stats`, the same source as the ticker, and they name the announced federations separately for the same reason the index does: "12 federations, 6 online" invites the reader to assume the other six are down. */} { stats.fedimint_total > 0 && (
{t('home.fedimint.title')} {t('home.fedimint.body', { total: stats.fedimint_total, online: stats.fedimint_online, announced: stats.fedimint_announced, })} {t('home.fedimint.link')}
) }

{t('home.latest.title')}

{t('home.latest.sub')} {/* The controls sit in the header row, never over the cards: the previous site put arrows on top of the reviews and they covered the text. They start hidden and the island only shows them when the track actually overflows, so with JavaScript off, or with three reviews on a wide screen, there is nothing here that does not work. */}
{ latest.length === 0 ? (

{t('home.latest.empty')}

) : ( ) }

{t('home.why.title')}

{t('home.why.signed.title')}

{t('home.why.verify.title')}

{t('home.why.verify.body')}

{t('home.why.checked.title')}

{t('home.why.checked.body')}

{t('home.cta.title')}

{t('home.cta.body')}