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:
parent
8394d006dd
commit
51e66727d9
2 changed files with 110 additions and 2 deletions
|
|
@ -31,6 +31,34 @@ export interface MintDirectoryEntry {
|
|||
nutCount?: number;
|
||||
tosUrl?: 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';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { useCashu } from '@/hooks/wallet/useCashu';
|
|||
import { useToast } from '@/hooks/useToast';
|
||||
import { isNativePlatform } from '@/lib/platform';
|
||||
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 {
|
||||
try {
|
||||
|
|
@ -112,6 +112,27 @@ function MintRow({
|
|||
</a>
|
||||
)}
|
||||
</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 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">
|
||||
{[
|
||||
{
|
||||
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',
|
||||
value:
|
||||
|
|
@ -369,8 +412,11 @@ export default function MintsPage() {
|
|||
label: 'NUTs supported',
|
||||
value: typeof selected.nutCount === 'number' ? String(selected.nutCount) : '—',
|
||||
},
|
||||
{
|
||||
label: 'Units',
|
||||
value: selected.units?.length ? selected.units.join(', ') : '—',
|
||||
},
|
||||
{ label: 'Version', value: selected.version ?? '—' },
|
||||
{ label: 'Last online', value: selected.lastOnlineAt ?? '—' },
|
||||
].map((f) => (
|
||||
<div
|
||||
key={f.label}
|
||||
|
|
@ -384,6 +430,40 @@ export default function MintsPage() {
|
|||
))}
|
||||
</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'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">
|
||||
{addedUrls.has(selected.url.toLowerCase()) ? (
|
||||
<Badge className="gap-1 border border-[#ccff00]/30 bg-[#ccff00]/15 text-[#ccff00]">
|
||||
|
|
|
|||
Loading…
Reference in a new issue