Improve write-review flow and require Node 22.18 for native TS stripping.
Gate login, reveal the form after a rating, and wire inline Rate this mint; drop --experimental-strip-types. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,22 +1,32 @@
|
||||
---
|
||||
/**
|
||||
* The write-a-review form: stars, body, identity row, actions.
|
||||
* The write-a-review form: a login gate, 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.
|
||||
*
|
||||
* Three states, only one of which is ever on screen:
|
||||
*
|
||||
* 1. **Gate.** No session and no NIP-07 extension: there is nothing to sign with, so
|
||||
* the dialog asks for a login before it asks for anything else. Writing a review
|
||||
* into a form that cannot publish it is the one outcome worth designing away.
|
||||
* 2. **Stars.** The rating is the whole first step. It is the one thing every review
|
||||
* must carry, it takes one tap, and it is what the rest of the form is *for*.
|
||||
* 3. **The rest.** Body, identity and publish, revealed once a rating is chosen.
|
||||
*
|
||||
* 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.
|
||||
* step back to the address field — and the lede and placeholder are props for the same
|
||||
* reason: "how did minting and melting work out" means nothing about a federation.
|
||||
*/
|
||||
import { useI18n, type Locale } from '../i18n';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
/** The one line under the heading, chosen per ecosystem by the caller. */
|
||||
lede: string;
|
||||
/** The body placeholder, chosen per ecosystem by the caller. */
|
||||
placeholder: string;
|
||||
/**
|
||||
@@ -29,7 +39,7 @@ interface Props {
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
const { locale, placeholder, idPrefix = 'review' } = Astro.props;
|
||||
const { locale, lede, placeholder, idPrefix = 'review' } = Astro.props;
|
||||
const t = useI18n(locale);
|
||||
|
||||
const bodyId = `${idPrefix}-body`;
|
||||
@@ -39,44 +49,74 @@ 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>
|
||||
<p class="modal-note" data-write-lede>{lede}</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>
|
||||
{/*
|
||||
The gate. Shown in place of everything below it when there is no signer at all, and
|
||||
never on a page load: whether a reader has one is a browser fact the island checks
|
||||
as the dialog opens. Its button is an ordinary `data-login-open`, so the same popup
|
||||
the header opens is the one that opens here.
|
||||
*/}
|
||||
<div class="write-gate" data-write-gate hidden>
|
||||
<p class="write-gate-title">{t('reviews.dialog.gate.title')}</p>
|
||||
<p class="write-gate-lead">{t('reviews.dialog.gate.lead')}</p>
|
||||
<div class="write-actions">
|
||||
<button type="button" class="btn" data-write-close>{t('common.cancel')}</button>
|
||||
<button type="button" class="btn primary" data-login-open>{t('login.open')}</button>
|
||||
</div>
|
||||
</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-steps" data-write-steps>
|
||||
<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>
|
||||
))
|
||||
}
|
||||
{/*
|
||||
The only "Choose a rating" on screen. It used to be said here and again beside
|
||||
the publish button, which is one prompt too many for one decision.
|
||||
*/}
|
||||
<span class="star-label" data-star-label aria-live="polite">
|
||||
{t('reviews.dialog.chooseRating')}
|
||||
</span>
|
||||
</fieldset>
|
||||
|
||||
<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 class="write-rest" data-write-rest hidden>
|
||||
<div class="write-id" data-write-identity></div>
|
||||
|
||||
<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>
|
||||
|
||||
{/*
|
||||
What pressing that button actually does, beside the button rather than in the
|
||||
heading: it is a consequence, and a consequence belongs next to the thing that
|
||||
causes it, not above a form nobody has filled in yet.
|
||||
*/}
|
||||
<p class="write-permanent">{t('reviews.dialog.permanent')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user