--- 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 { fetchFedimint, fetchFedimints, fetchHealth, fetchLnurlMint, fetchLnurlMints, 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, lnurlMints, stats, health] = await Promise.all([ fetchMints(), fetchFedimints(), fetchLnurlMints(), fetchStats(), fetchHealth(), ]); const top = mints.slice(0, 6); const topFederations = federations.slice(0, 6); const topLnurl = lnurlMints.slice(0, 6); /* * The reviews strip resolves against every ecosystem, 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. The * same is true of an LNURL mint, which today has the further handicap that 38174 is a * proposed kind with barely any events on the network at all. */ const reviewTargets = feedTargets(mints, federations, lnurlMints); /* * 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, fediDetails, lnurlDetails, 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))), Promise.all(topFederations.map((federation) => fetchFedimint(federation.host).catch(() => null))), Promise.all(topLnurl.map((mint) => fetchLnurlMint(mint.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)); // No capability read for federations: they publish no NUT switches to draw a chip from. const fediSentiments = fediDetails.map((d) => (d ? sentiment(d.rating_distribution) : undefined)); const lnurlSentiments = lnurlDetails.map((d) => (d ? sentiment(d.rating_distribution) : undefined)); /* * An LNURL card's chip comes from two probed facts rather than from NUT switches: an * advertised withdraw ceiling of zero, and a Lightning node the mint cannot reach. */ const lnurlChips = lnurlDetails.map((d) => d ? { max_withdrawable_msat: d.max_withdrawable_msat ?? null, funding_available: d.funding_available ?? null, } : 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 grid: the mint grid's twin, under it and above the reviews. The same ranked cards as /fedimints, with the same real sentiment bars the Cashu grid gets, so a reader moving between the two sections does not have to re-learn a card. */} { topFederations.length > 0 && (

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

{t('home.topFedimints.sub')} {t('home.topFedimints.all', { n: federations.length })}
{topFederations.map((federation, i) => ( ))}
) } {/* The LNURL grid: the third ecosystem strip, in the same order the nav lists them. Same ranked cards as /lnurl-mints, same real sentiment bars, and the same chip rule — so a reader moving down the page meets one card three times rather than three cards once each. */} { topLnurl.length > 0 && (

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

{t('home.topLnurl.sub')} {t('home.topLnurl.all', { n: stats.lnurl_total })}
{topLnurl.map((mint, i) => ( ))}
) }

{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')}