Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
This commit is contained in:
@@ -3,8 +3,9 @@ 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 { fetchHealth, fetchMint, fetchMints, fetchStats } from '../../lib/api';
|
||||
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';
|
||||
@@ -22,15 +23,27 @@ const { locale } = Astro.props;
|
||||
const t = useI18n(locale);
|
||||
const f = formatters(t);
|
||||
|
||||
const [mints, stats, health] = await Promise.all([fetchMints(), fetchStats(), fetchHealth()]);
|
||||
const [mints, federations, stats, health] = await Promise.all([
|
||||
fetchMints(),
|
||||
fetchFedimints(),
|
||||
fetchStats(),
|
||||
fetchHealth(),
|
||||
]);
|
||||
|
||||
const top = mints.slice(0, 6);
|
||||
const hostByUrl = new Map(mints.map((m) => [m.url, m.host]));
|
||||
|
||||
/*
|
||||
* 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 `hostByUrl`, which the mint list above already gave us — so they wait
|
||||
* together rather than one behind the other.
|
||||
* 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
|
||||
@@ -39,7 +52,7 @@ const hostByUrl = new Map(mints.map((m) => [m.url, m.host]));
|
||||
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(hostByUrl, 10),
|
||||
fetchLatestReviews(reviewTargets, 10),
|
||||
]);
|
||||
|
||||
const sentiments = details.map((d) => (d ? sentiment(d.rating_distribution) : undefined));
|
||||
@@ -150,6 +163,49 @@ const signedBody = t('home.why.signed.body', {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/*
|
||||
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 && (
|
||||
<div class="section eco-strip-wrap">
|
||||
<a class="eco-strip" href={localePath('/fedimints', locale)} data-reveal>
|
||||
<span class="eco-mark" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="6" r="2.4" /><circle cx="5.5" cy="17" r="2.4" /><circle cx="18.5" cy="17" r="2.4" />
|
||||
<path d="M10.6 8 6.9 14.9M13.4 8l3.7 6.9M8 17.4h8" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="eco-body">
|
||||
<span class="eco-title">{t('home.fedimint.title')}</span>
|
||||
<span class="eco-sub">
|
||||
{t('home.fedimint.body', {
|
||||
total: stats.fedimint_total,
|
||||
online: stats.fedimint_online,
|
||||
announced: stats.fedimint_announced,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
<span class="eco-go">
|
||||
{t('home.fedimint.link')}
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<section class="section" aria-labelledby="latest-reviews-title" id="latest-reviews">
|
||||
<div class="sec-head" data-reveal>
|
||||
<h2 class="sec-title" id="latest-reviews-title">{t('home.latest.title')}</h2>
|
||||
@@ -189,7 +245,11 @@ const signedBody = t('home.why.signed.body', {
|
||||
const initial = (name?.[0] ?? review.npub[5] ?? '?').toUpperCase();
|
||||
const filled = review.rating === null ? 0 : Math.round(review.rating);
|
||||
return (
|
||||
<a class="rev-card" href={localePath(`/mint/${review.mint_host}`, locale)} data-reveal>
|
||||
<a
|
||||
class="rev-card"
|
||||
href={localePath(targetPath({ type: review.mint_type, host: review.mint_host }), locale)}
|
||||
data-reveal
|
||||
>
|
||||
<span class="rc-top">
|
||||
<span class="rc-avatar-wrap">
|
||||
<span class="rc-avatar" style={`background:${iconGradient(review.pubkey)}`} aria-hidden="true">
|
||||
@@ -360,6 +420,47 @@ const signedBody = t('home.why.signed.body', {
|
||||
}
|
||||
.qf:hover { color: var(--text); border-color: #38342A; background: var(--card); }
|
||||
|
||||
/* ---------- fedimint strip ---------- */
|
||||
/*
|
||||
* One card wide enough to be a section and short enough not to be one. It sits in a
|
||||
* `.section` for the page's gutter and max width, but carries no heading of its own:
|
||||
* a `<h2>` here would give a signpost the same weight as the mint grid above it.
|
||||
*/
|
||||
.eco-strip-wrap { padding-top: 0; }
|
||||
.eco-strip {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
border: 1px solid var(--line); border-radius: var(--radius);
|
||||
background: var(--card); padding: 18px 20px;
|
||||
transition: border-color var(--dur-quick), background var(--dur-quick), transform var(--dur-quick);
|
||||
}
|
||||
.eco-strip:hover { border-color: #38342A; background: var(--card2); }
|
||||
.eco-strip:hover .eco-go { color: var(--amber); }
|
||||
.eco-strip:hover .eco-go svg { transform: translateX(2px); }
|
||||
|
||||
.eco-mark {
|
||||
width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--amber); background: rgba(240, 169, 59, .09);
|
||||
border: 1px solid rgba(240, 169, 59, .22);
|
||||
}
|
||||
.eco-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||
.eco-title { font-family: var(--display); font-size: 16.5px; font-weight: 600; letter-spacing: -.01em; color: var(--text); }
|
||||
.eco-sub { font-size: 13.5px; color: var(--muted); }
|
||||
.eco-go {
|
||||
margin-left: auto; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13.5px; font-weight: 500; color: var(--muted);
|
||||
transition: color var(--dur-quick);
|
||||
}
|
||||
.eco-go svg { transition: transform var(--dur-quick); }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
/* The arrow label drops; the whole row is the link and the chevron still points. */
|
||||
.eco-strip { gap: 13px; }
|
||||
.eco-go { font-size: 0; gap: 0; }
|
||||
.eco-go svg { font-size: 15px; }
|
||||
}
|
||||
|
||||
/* ---------- latest reviews carousel ---------- */
|
||||
/*
|
||||
* A scroll-snap track, not a JavaScript slider: native touch and trackpad swiping,
|
||||
|
||||
Reference in New Issue
Block a user