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:
+101
-15
@@ -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'),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user