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
+82
View File
@@ -0,0 +1,82 @@
---
/**
* The write-a-review form: stars, body, identity row, actions.
*
* One markup, two dialogs. The reviews panel on a mint page opens it directly; the
* review-by-URL dialog on an index page opens it as its second step, once the reader's
* pasted address has been indexed. Its behaviour lives in `lib/write-review.ts`, and
* every hook below (`data-write-*`) is what that module binds to.
*
* Nothing here knows which ecosystem is being reviewed. The heading is a slot because
* the two dialogs have genuinely different heads — one names the mint, the other has a
* step back to the address field — and the placeholder is a prop for the same reason
* the panel already had one: "how did minting and melting work out" means nothing about
* a federation.
*/
import { useI18n, type Locale } from '../i18n';
interface Props {
locale: Locale;
/** The body placeholder, chosen per ecosystem by the caller. */
placeholder: string;
/**
* Prefix for the two element ids in here.
*
* A mint page carries both dialogs at once — the panel's, and the one the header
* button opens — so the ids cannot be constants. `for`/`aria-describedby` are the
* whole reason they exist; nothing styles by id.
*/
idPrefix?: string;
}
const { locale, placeholder, idPrefix = 'review' } = Astro.props;
const t = useI18n(locale);
const bodyId = `${idPrefix}-body`;
const countId = `${idPrefix}-count`;
---
<form method="dialog" class="modal-body write-form" data-write-form novalidate>
<slot name="head" />
<p class="modal-note">{t('reviews.dialog.note')}</p>
<div class="write-id" data-write-identity></div>
<fieldset class="star-picker" data-star-picker>
<legend class="sr-only">{t('reviews.dialog.ratingLegend')}</legend>
{
[1, 2, 3, 4, 5].map((value) => (
<label class="star-option">
<input type="radio" name="rating" value={value} required />
<span aria-hidden="true">★</span>
<span class="sr-only">{t('reviews.dialog.starsSr', { n: value })}</span>
</label>
))
}
<span class="star-label" data-star-label aria-live="polite">
{t('reviews.dialog.chooseRating')}
</span>
</fieldset>
<label class="write-label" for={bodyId}>{t('reviews.dialog.bodyLabel')}</label>
<div class="write-box">
<textarea id={bodyId} name="content" rows="5" aria-describedby={countId} placeholder={placeholder}
></textarea>
<span class="write-count" id={countId} data-char-count>
{t('reviews.dialog.charCount', { n: 0, max: 2000 })}
</span>
</div>
<p class="write-status" data-write-status hidden role="status"></p>
<p class="write-error" data-write-error hidden role="alert"></p>
<div class="write-actions">
<span class="write-why" data-write-why></span>
<button type="button" class="btn" data-write-close>{t('common.cancel')}</button>
<button type="submit" class="btn primary" data-write-submit disabled>
<span class="write-spinner" aria-hidden="true"></span>
<span data-write-submit-label>{t('reviews.dialog.submit')}</span>
</button>
</div>
</form>