Ship LNURL mint pages, indexing UI, and reviews rewrite.

Add lnurl list/detail routes, OG fixtures, i18n strings, and the write/
index client flows so the site surfaces the new mint type end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:43 +02:00
co-authored by Cursor
parent 2a9444942b
commit c74c7fc187
66 changed files with 7603 additions and 1241 deletions
+101 -15
View File
@@ -29,6 +29,8 @@ const AMBER = '#F0A93B';
const VIOLET = '#A78BFA';
const MINT = '#5FD68F';
const RED = '#E06450';
/** The LNURL ecosystem label. The only cool blue here — see DESIGN.md. */
const LNURL = '#4EA8DE';
const GROTESK = 'Space Grotesk';
const INTER = 'Inter';
@@ -137,22 +139,29 @@ function statusChip(model) {
}
}
/** The type badge that makes a federation card unmistakable at thumbnail size. */
function fedimintBadge() {
/**
* The type badge that makes a non-Cashu card unmistakable at thumbnail size.
*
* Three things separate the three ecosystems at 300px wide, and the badge is only one
* of them: the word itself, the badge's colour, and the glow behind the whole card all
* change together. Cashu gets no badge at all, which is its own signal — it is the
* default and the majority of the index.
*/
function typeBadge(label, color, borderColor) {
return h(
'div',
{
display: 'flex',
padding: '9px 14px',
borderRadius: '9px',
border: `1.5px solid rgba(167,139,250,.5)`,
color: VIOLET,
border: `1.5px solid ${borderColor}`,
color,
fontFamily: MONO,
fontWeight: 500,
fontSize: '18px',
letterSpacing: '3px',
},
'FEDIMINT',
label,
);
}
@@ -380,15 +389,86 @@ function thirdCellFedimint(model) {
);
}
/** The background glow: amber for a healthy card, red-tinted for trouble, violet for Fedimint. */
/**
* Third cell, LNURL: the withdraw range — never NUT limits, never a module list.
*
* This is the number that answers an LNURL reader's actual question: how much can one
* of this mint's notes be worth? It is shown in sats, converted from millisatoshi in
* the model, and it is a *range* rather than a pair of independent bounds, which is
* why the two rows read "min"/"max" the way the Cashu limits cell does.
*
* The lightning address takes the space the Fedimint cell gives its network badge: it
* is the one string on the whole card someone might copy out of a screenshot.
*/
function thirdCellLnurl(model) {
const base = {
display: 'flex',
flexDirection: 'column',
flex: 1.1,
padding: '30px 36px',
borderLeft: `1px solid ${LINE_SOFT}`,
};
if (model.warning) {
const color = model.warning.severity === 'critical' ? RED : AMBER;
return h(
'div',
{ ...base, backgroundColor: model.warning.severity === 'critical' ? 'rgba(224,100,80,.06)' : 'rgba(240,169,59,.05)' },
cellLabel('WARNING', color),
h('div', { fontFamily: GROTESK, fontWeight: 700, fontSize: '34px', lineHeight: 1.05, color }, model.warning.label),
h('div', { fontFamily: INTER, fontWeight: 500, fontSize: '19px', color: MUTED, marginTop: '14px' }, model.warning.sub),
);
}
const lines = [];
if (model.withdraw) {
if (model.withdraw.min !== null) lines.push(['min', `${compact(model.withdraw.min)} sat`]);
if (model.withdraw.max !== null) lines.push(['max', `${compact(model.withdraw.max)} sat`]);
}
return h(
'div',
base,
cellLabel('WITHDRAW'),
lines.length === 0
? h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '24px', color: MUTED, marginTop: '8px' }, 'Not published')
: h(
'div',
{ display: 'flex', flexDirection: 'column', gap: '10px', marginTop: '4px' },
...lines.map(([k, v]) =>
h(
'div',
{ display: 'flex', alignItems: 'baseline', gap: '14px' },
h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '20px', color: FAINT }, k),
h('div', { fontFamily: MONO, fontWeight: 500, fontSize: '27px', color: TEXT }, v),
),
),
),
model.address &&
h(
'div',
{ fontFamily: MONO, fontWeight: 400, fontSize: '18px', color: FAINT, marginTop: '16px', maxWidth: '300px' },
fitMono(model.address, 30),
),
);
}
/** The background glow: amber healthy, red for trouble, violet Fedimint, blue LNURL. */
function glow(model) {
const danger =
model.status === 'offline' || (model.type === 'cashu' && model.warning && model.warning.severity === 'critical');
model.status === 'offline' ||
(model.type !== 'fedimint' && model.warning && model.warning.severity === 'critical');
const color = danger
? 'rgba(224,100,80,.13)'
: model.type === 'fedimint'
? 'rgba(167,139,250,.11)'
: 'rgba(240,169,59,.10)';
: model.type === 'lnurl'
// Deliberately the strongest of the three. Amber and violet only have to
// separate a card from the site's own background; this one has to separate an
// LNURL card from a Fedimint card at 300px, where the badge text is a smudge
// and the tint is the first thing a reader actually resolves.
? 'rgba(78,168,222,.20)'
: 'rgba(240,169,59,.10)';
return h('div', {
position: 'absolute',
top: '-260px',
@@ -414,10 +494,12 @@ function brandRow(tagline) {
/** One mint or federation card. `iconData` is a data: URI or null. */
export function mintCard(model, iconData) {
const isFed = model.type === 'fedimint';
const isLnurl = model.type === 'lnurl';
const monoLine = isFed ? model.shortId : fitMono(model.domain, 46);
// Room beside the icon: canvas minus padding, icon, gaps and the chips.
const chipRoom = 240 + (isFed ? 180 : 0);
// Room beside the icon: canvas minus padding, icon, gaps and the chips. A badge is
// ~180px for FEDIMINT and ~120px for the shorter LNURL.
const chipRoom = 240 + (isFed ? 180 : isLnurl ? 120 : 0);
const { size, clamp } = nameSize(model.name, 1200 - 128 - 148 - chipRoom);
return h(
@@ -433,7 +515,7 @@ export function mintCard(model, iconData) {
fontFamily: INTER,
},
glow(model),
brandRow(isFed ? 'Fedimint reviews' : 'Cashu mint reviews'),
brandRow(isFed ? 'Fedimint reviews' : isLnurl ? 'LNURL mint reviews' : 'Cashu mint reviews'),
h(
'div',
{ display: 'flex', alignItems: 'center', gap: '32px', marginTop: '52px', flexGrow: 1 },
@@ -459,7 +541,8 @@ export function mintCard(model, iconData) {
model.name,
),
statusChip(model),
isFed && fedimintBadge(),
isFed && typeBadge('FEDIMINT', VIOLET, 'rgba(167,139,250,.5)'),
isLnurl && typeBadge('LNURL', LNURL, 'rgba(78,168,222,.5)'),
),
h('div', { fontFamily: MONO, fontWeight: 400, fontSize: '25px', color: MUTED, marginTop: '14px' }, monoLine),
// The Fedimint soft warning is a muted line, deliberately not a red cell.
@@ -479,7 +562,7 @@ export function mintCard(model, iconData) {
},
ratingCell(model),
reviewsCell(model),
isFed ? thirdCellFedimint(model) : thirdCellCashu(model),
isFed ? thirdCellFedimint(model) : isLnurl ? thirdCellLnurl(model) : thirdCellCashu(model),
),
);
}
@@ -525,13 +608,16 @@ export function defaultCard(model) {
h(
'div',
{ fontFamily: INTER, fontWeight: 500, fontSize: '28px', color: MUTED, marginTop: '14px' },
'Cashu mint and Fedimint reviews, signed on Nostr',
'Cashu, Fedimint and LNURL mint reviews, signed on Nostr',
),
h(
'div',
{ display: 'flex', gap: '86px', marginTop: '64px' },
// Four stats where there were three, so the gap tightens rather than the row
// running past the card's own margins.
{ display: 'flex', gap: '64px', marginTop: '64px' },
stat(model.mints, 'Cashu mints'),
stat(model.federations, 'federations'),
stat(model.lnurl, 'LNURL mints'),
stat(model.reviews, 'signed reviews'),
),
);