feat(mints): surface MintRadar's community ratings, reliability and audit data

The directory we already fetch returns review ratings, reliability, uptime and
liveness-audit stats that the client was throwing away (it only typed ~12 of ~35
fields). Now:

- MintDirectoryEntry types the community fields (rating/weighted rating/count,
  reliabilityScore, uptime 24h+7d, audit volume + recent errors, serverLocation,
  units, mint/melt methods, pubkey, discoveredAt, lastError).
- Row shows the rating (e.g. 4.6 (30)) plus reliability and 24h uptime.
- Detail sheet shows rating, reliability, uptime, location, latency, NUTs,
  units, version, plus a liveness-audit line and an honest note that written
  reviews live on MintRadar (linked), not in the directory API.

Written comments are genuinely not in MintRadar's /api/mints/known (probed a few
plausible review endpoints - all 404), so the next step for in-app comments
would be Nostr-native mint reviews.
This commit is contained in:
SkateSpots 2026-09-29 18:02:57 +07:00
parent 8394d006dd
commit 51e66727d9
2 changed files with 110 additions and 2 deletions

View file

@ -31,6 +31,34 @@ export interface MintDirectoryEntry {
nutCount?: number; nutCount?: number;
tosUrl?: string; tosUrl?: string;
lastOnlineAt?: string; lastOnlineAt?: string;
// ── Community signal MintRadar also returns (previously unused) ────────────
/** Average community rating, 0–5. */
reviewAvgRating?: number;
/** How many community ratings back the average. */
reviewCount?: number;
/** Rating after MintRadar's own weighting. */
reviewWeightedRating?: number;
/** 0–100 composite reliability score. */
reliabilityScore?: number;
uptimePct24h?: number;
uptimePct7d?: number;
/** Liveness audit volume — tokens actually minted/melted through it. */
auditNMints?: number;
auditNMelts?: number;
auditNErrors?: number;
auditRecentErrors?: number;
auditRecentTotal?: number;
auditCheckedAt?: string;
serverLocation?: string;
units?: string[];
mintMethods?: Array<{ unit?: string; method?: string; min_amount?: number; max_amount?: number }>;
meltMethods?: Array<{ unit?: string; method?: string; min_amount?: number; max_amount?: number }>;
/** The mint's own Nostr announcement key, when it publishes one. */
pubkey?: string;
nostrAnnouncedAt?: string;
discoveredAt?: string;
lastError?: string | null;
} }
const SITE_ORIGIN = 'https://skate-spots.org'; const SITE_ORIGIN = 'https://skate-spots.org';

View file

@ -12,7 +12,7 @@ import { useCashu } from '@/hooks/wallet/useCashu';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { isNativePlatform } from '@/lib/platform'; import { isNativePlatform } from '@/lib/platform';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Search, Coins, ExternalLink, Plus, Loader2, Check, ShieldAlert, Activity, ChevronRight, Copy, Info } from 'lucide-react'; import { Search, Coins, ExternalLink, Plus, Loader2, Check, ShieldAlert, Activity, ChevronRight, Copy, Info, Star } from 'lucide-react';
function hostOf(url: string): string { function hostOf(url: string): string {
try { try {
@ -112,6 +112,27 @@ function MintRow({
</a> </a>
)} )}
</div> </div>
{/* Community signal — ratings + reliability from MintRadar. */}
{((typeof mint.reviewAvgRating === 'number' && (mint.reviewCount ?? 0) > 0) ||
typeof mint.reliabilityScore === 'number' ||
typeof mint.uptimePct24h === 'number') && (
<div className="mt-1.5 flex flex-wrap items-center gap-x-2.5 gap-y-0.5 text-[11px]">
{typeof mint.reviewAvgRating === 'number' && (mint.reviewCount ?? 0) > 0 && (
<span className="inline-flex items-center gap-1 font-bold text-amber-400">
<Star className="h-3 w-3 fill-current" />
{mint.reviewAvgRating.toFixed(1)}
<span className="font-normal text-gray-500">({mint.reviewCount})</span>
</span>
)}
{typeof mint.reliabilityScore === 'number' && (
<span className="text-gray-400">reliability {mint.reliabilityScore}/100</span>
)}
{typeof mint.uptimePct24h === 'number' && (
<span className="text-gray-400">{Math.round(mint.uptimePct24h)}% up · 24h</span>
)}
</div>
)}
</div> </div>
<div className="shrink-0 flex items-center gap-1.5"> <div className="shrink-0 flex items-center gap-1.5">
@ -358,6 +379,28 @@ export default function MintsPage() {
<div className="mt-4 grid grid-cols-2 gap-2"> <div className="mt-4 grid grid-cols-2 gap-2">
{[ {[
{
label: 'Community rating',
value:
typeof selected.reviewAvgRating === 'number' && (selected.reviewCount ?? 0) > 0
? `${selected.reviewAvgRating.toFixed(1)} / 5 · ${selected.reviewCount}`
: '—',
},
{
label: 'Reliability',
value:
typeof selected.reliabilityScore === 'number'
? `${selected.reliabilityScore} / 100`
: '—',
},
{
label: 'Uptime 24h / 7d',
value:
typeof selected.uptimePct24h === 'number' && typeof selected.uptimePct7d === 'number'
? `${Math.round(selected.uptimePct24h)}% / ${Math.round(selected.uptimePct7d)}%`
: '—',
},
{ label: 'Location', value: selected.serverLocation ?? '—' },
{ {
label: 'Latency', label: 'Latency',
value: value:
@ -369,8 +412,11 @@ export default function MintsPage() {
label: 'NUTs supported', label: 'NUTs supported',
value: typeof selected.nutCount === 'number' ? String(selected.nutCount) : '—', value: typeof selected.nutCount === 'number' ? String(selected.nutCount) : '—',
}, },
{
label: 'Units',
value: selected.units?.length ? selected.units.join(', ') : '—',
},
{ label: 'Version', value: selected.version ?? '—' }, { label: 'Version', value: selected.version ?? '—' },
{ label: 'Last online', value: selected.lastOnlineAt ?? '—' },
].map((f) => ( ].map((f) => (
<div <div
key={f.label} key={f.label}
@ -384,6 +430,40 @@ export default function MintsPage() {
))} ))}
</div> </div>
<div className="mt-3 space-y-1.5 text-[11px] leading-relaxed text-gray-500">
<p className="flex items-start gap-2">
<Star className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-400" />
<span>
{typeof selected.reviewAvgRating === 'number' && (selected.reviewCount ?? 0) > 0
? `Rated ${selected.reviewAvgRating.toFixed(1)}/5 by ${selected.reviewCount} people on MintRadar.`
: 'No community rating yet on MintRadar.'}{' '}
Written reviews aren&apos;t part of the directory —{' '}
<a
href="https://mintradar.org/"
target="_blank"
rel="noopener noreferrer"
className="text-gray-300 underline underline-offset-2 hover:text-white"
>
open MintRadar
</a>{' '}
to read or leave one.
</span>
</p>
{typeof selected.auditNMints === 'number' && (
<p className="flex items-start gap-2">
<Activity className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#ccff00]" />
<span>
Liveness audit: {selected.auditNMints.toLocaleString()} mints and{' '}
{(selected.auditNMelts ?? 0).toLocaleString()} melts tested
{selected.auditRecentTotal
? ` · ${selected.auditRecentErrors ?? 0}/${selected.auditRecentTotal} recent errors`
: ''}
.
</span>
</p>
)}
</div>
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
{addedUrls.has(selected.url.toLowerCase()) ? ( {addedUrls.has(selected.url.toLowerCase()) ? (
<Badge className="gap-1 border border-[#ccff00]/30 bg-[#ccff00]/15 text-[#ccff00]"> <Badge className="gap-1 border border-[#ccff00]/30 bg-[#ccff00]/15 text-[#ccff00]">