--- import { baseUrlFromKey, federationIdFromSlug, getMintWarnings, mintChip, type LnurlDetail, type MintCapabilities, type MintListItem, } from '@cashumints/shared'; import { iconUrl } from '../lib/api'; import { targetPath } from '../lib/feed-resolve'; import { displayDomain, iconGradient, initials, transitionName } from '../lib/format'; import { localePath, useI18n } from '../i18n'; import { formatters, starString } from '../i18n/format'; import { chipStrings, statusLabel, warningOptions } from '../i18n/mint'; import { pageLocale } from '../i18n/paths'; interface Props { mint: MintListItem; rank?: number; /** Sentiment split, when the caller has the distribution (home page top mints). */ sentiment?: { pos: number; neg: number }; /** * Entrance delay in ms, for a grid of known size that arrives as one gesture (the * home page's six). The long index at /mints leaves this unset and the reveal * utility staggers whatever scrolls into view together instead, so a card in the * fortieth row is not waiting on the position of the thirty-ninth. */ revealDelay?: number; /** * NUT-04 / NUT-05 switches, read from the mint's cached info by whoever fetched it. * The list payload does not carry them, so a page that wants the chip reads them * from the detail it already has. Left off, the card simply has no chip. */ capabilities?: MintCapabilities | null; /** * The LNURL states a chip can be drawn from, for a caller that already has the * detail. The Cashu counterpart is `capabilities` above, and this is separate rather * than merged with it because the two ecosystems' chips are read from entirely * different facts: NUT switches there, an advertised withdraw ceiling and a reachable * Lightning node here. Left off, an LNURL card simply has no chip. */ lnurl?: Pick | null; } const { mint, rank, sentiment, revealDelay, capabilities, lnurl } = Astro.props; const { locale } = pageLocale(Astro); const t = useI18n(locale); const f = formatters(t); /* * One card, all three ecosystems. * * A federation has no URL, so the second line cannot be a domain: `fedimint:aeca6c…` * is a database key and means nothing to a reader. It shows the federation id instead, * shortened, which is the thing that actually identifies it — and which is what the * search box below matches on, so pasting an id finds its federation. * * An LNURL mint does have a URL, but its row key carries an `lnurl:` scheme in front of * it, so the domain comes out of the key rather than off it directly. A reader should * see `lnurl.21mint.me`, not `lnurl:https://lnurl.21mint.me`. * * Everything else on the card is identical, and deliberately: rating, review count, * sentiment bar and status all mean the same thing for all three, and a reader moving * between the three indexes should not have to re-learn a card. */ const fedimint = mint.type === 'fedimint'; const lnurlBase = baseUrlFromKey(mint.url); const domain = fedimint ? federationIdFromSlug(mint.host) : displayDomain(lnurlBase ?? mint.url); const name = fedimint ? mint.name ?? t('card.unnamedFederation') : mint.name ?? domain.split('/')[0] ?? domain; const icon = iconUrl(mint.icon); const offline = mint.status === 'offline'; /** Announced on Nostr and never confirmed by any check. Federations only. */ const announced = mint.status === 'announced'; // One route per ecosystem, from the same table the review feed's links use, so a card // and a feed row can never disagree about where a listing lives. const href = localePath(targetPath(mint), locale); /* Two words at most. Offline already has its own card styling, so only the states a glance at the status cannot reveal earn a chip: a mint that takes deposits it cannot pay back out, or one that has stopped moving sats in either direction. The mint page carries the explanation. */ const chipSource = capabilities || lnurl ? { type: mint.type, status: mint.status, last_online: mint.last_online, capabilities: capabilities ?? null, ...(lnurl ?? {}), } : null; const chip = chipSource ? mintChip(getMintWarnings(chipSource, warningOptions(f)), chipStrings(t)) : null; const status = statusLabel(mint.status, t); /* * The mint's own name is never translated. It comes from the mint's /v1/info, and a * mint called "Bolverker" is called that in every language. Same for the domain, which * is an address. */ // Fall back to the rating split when no distribution was passed: a mint with a 4.6 // average is roughly 92% positive, which is what the bar is communicating. const bar = sentiment ?? (mint.rating_avg === null ? { pos: 0, neg: 0 } : { pos: Math.round(((mint.rating_avg - 1) / 4) * 100), neg: 100 - Math.round(((mint.rating_avg - 1) / 4) * 100), }); --- {/* The icon and the name carry the view transition names their twins in the mint page header carry, so clicking a card morphs them into place instead of cross-fading the whole page over them. They are held in data attributes rather than applied with `transition:name`, and scripts/reveal.ts moves the one card being clicked onto the real property. A named element is captured and animated separately, so naming all 58 cards on /mints would hand the browser 116 textures to composite on every navigation, for 114 of which the answer is "fade it out with everything else". */}
{ icon ? ( ) : ( ) } {name} {domain} {rank !== undefined && #{rank}}
{ mint.rating_avg === null ? ( {t('card.noRatings')} ) : ( {/* One decimal, with this locale's decimal separator: 4.6 and 4,6. */} {f.decimal(mint.rating_avg)} ) } {t('card.reviews', { n: mint.review_count })}
{/* Both segments live in one wrapper so the bar grows from the card's left edge as a single object (scaleX on the wrapper) rather than as two bars starting in two places. The widths themselves never animate. */}
{status} {chip && {chip.label}} { /* "Announced" has no "last seen" to print, and printing one anyway is exactly the fake status this site refuses to show. What it has instead is the fact that nothing has checked it, said in as many words. */ announced ? t('card.notChecked') : offline ? mint.last_online ? t('card.lastSeen', { when: f.relative(mint.last_online) }) : t('card.neverSeen') : mint.last_review_at ? t('card.reviewed', { when: f.relative(mint.last_review_at) }) : t('card.noReviews') }