Expand i18n locales and improve reviews UI
Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@ 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 { fetchFedimint, 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';
|
||||
@@ -31,6 +31,7 @@ const [mints, federations, stats, health] = await Promise.all([
|
||||
]);
|
||||
|
||||
const top = mints.slice(0, 6);
|
||||
const topFederations = federations.slice(0, 6);
|
||||
|
||||
/*
|
||||
* The reviews strip resolves against both ecosystems, because its heading is "latest
|
||||
@@ -49,15 +50,18 @@ const reviewTargets = feedTargets(mints, federations);
|
||||
* 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([
|
||||
const [details, fediDetails, 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))),
|
||||
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 description = t('home.description', {
|
||||
mints: stats.mints_total,
|
||||
@@ -164,45 +168,26 @@ const signedBody = t('home.why.signed.body', {
|
||||
</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.
|
||||
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.
|
||||
*/}
|
||||
{
|
||||
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>
|
||||
topFederations.length > 0 && (
|
||||
<section class="section" aria-labelledby="top-fedimints">
|
||||
<div class="sec-head" data-reveal>
|
||||
<h2 class="sec-title" id="top-fedimints">{t('home.topFedimints.title')}</h2>
|
||||
<span class="sec-sub">{t('home.topFedimints.sub')}</span>
|
||||
<a class="sec-link" href={localePath('/fedimints', locale)}>
|
||||
{t('home.topFedimints.all', { n: federations.length })}
|
||||
</a>
|
||||
</div>
|
||||
<div class="mint-grid">
|
||||
{topFederations.map((federation, i) => (
|
||||
<MintCard mint={federation} rank={i + 1} sentiment={fediSentiments[i]} revealDelay={i * 50} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -420,47 +405,6 @@ 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