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;
|
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';
|
||||||
|
|
|
||||||
|
|
@ -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'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]">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue