Map audit: fix dead trusted-first sort + broken stash-card borders (1.2.36)

Two real bugs surfaced auditing the Map page.

1. The "trusted-first" soft sort was silently dead for the default
   config. filteredSpots ran a standalone .sort() to float trusted
   content up, then IMMEDIATELY ran a second unconditional .sort() by
   sortBy — which re-ordered everything and wiped the trust ordering.
   For a logged-in user with ≥5 follows the smart default is
   trustFilter='trusted' + sortBy='recent', so the documented
   "sort trusted first, badge unverified" behaviour never actually
   happened. Fixed by folding the trust tier into the MAIN comparator as
   its primary key, with the chosen sortBy as the tiebreaker within each
   tier. Strict modes (verified/following) still hard-filter as before.

2. MiniStashCard (gear pins in the map drawer) built its border colour
   with an interpolated Tailwind class (`border-${colour}-500/40`) that
   the JIT compiler can't see, so it was never generated — dead class
   emitting an invalid string into the DOM. The inline
   style={{ borderColor }} that actually worked was also applied
   unconditionally, overriding the intended cyan border on WISHLIST
   items with the rarity colour. Now: wishlist keeps its static cyan
   border; gear uses the rarity colour via inline style only.

Geolocation (native vs web, denied/timeout/no-provider handling),
batched trust/status/comment/weather queries, and the layer plumbing
into SpotMap were all audited and are sound.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-07 23:03:34 +07:00
parent 719dd2c4a8
commit 85cb76b458
2 changed files with 30 additions and 23 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.2.35", "version": "1.2.36",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "scripts": {

View file

@ -98,10 +98,16 @@ function MiniStashCard({ item, onDelete }: { item: StashItem; onDelete: () => vo
const hasImage = item.images && item.images.length > 0; const hasImage = item.images && item.images.length > 0;
return ( return (
<div className={cn( <div
'relative p-3 rounded-lg border-2 bg-black/60 transition-all hover:bg-black/80', className={cn(
isWishlist ? 'border-cyan-500/40' : `border-${rarity === 'common' ? 'gray' : rarity === 'uncommon' ? 'green' : rarity === 'rare' ? 'blue' : rarity === 'epic' ? 'purple' : 'yellow'}-500/40` 'relative p-3 rounded-lg border-2 bg-black/60 transition-all hover:bg-black/80',
)} style={{ borderColor: rarityTier.color + '40' }}> // Wishlist keeps a fixed cyan border; gear uses its rarity colour (applied
// via inline style below, since Tailwind's JIT can't see dynamically
// interpolated `border-${colour}` class names).
isWishlist && 'border-cyan-500/40',
)}
style={isWishlist ? undefined : { borderColor: rarityTier.color + '40' }}
>
{/* Wishlist badge */} {/* Wishlist badge */}
{isWishlist && ( {isWishlist && (
<Badge className="absolute -top-2 -left-1 bg-cyan-500 text-black text-[9px] px-1.5 py-0"> <Badge className="absolute -top-2 -left-1 bg-cyan-500 text-black text-[9px] px-1.5 py-0">
@ -833,27 +839,28 @@ export default function MapPage() {
return true; return true;
}); });
// Apply trust filter — when 'trusted', sort trusted content to the top // Apply trust filter.
// rather than hiding untrusted content (avoids penalising new users/anon browsers) // • 'trusted' (soft): keep everything visible but float trusted content to
// Strict filtering (verified/following) still hides low-trust content as expected // the top. This must be folded into the MAIN sort below as its primary
if (trustScores && trustFilter !== 'all') { // key — running it as a separate .sort() first is pointless because the
if (trustFilter === 'trusted') { // sort() that follows would immediately re-order everything and wipe it
// Soft mode: sort trusted first, keep everything visible // out (which it silently did for the default trusted+recent combo).
filtered.sort((a, b) => { // • 'verified'/'following' (strict): actually hide low-trust content.
const scoreA = getTrustScore(a.pubkey) ?? -1; const softTrust = trustFilter === 'trusted';
const scoreB = getTrustScore(b.pubkey) ?? -1; if (trustScores && trustFilter !== 'all' && !softTrust) {
const trustedA = scoreA >= 60 ? 1 : 0; filtered = filterByTrustLevel(filtered, trustFilter, getTrustScore);
const trustedB = scoreB >= 60 ? 1 : 0;
return trustedB - trustedA;
});
} else {
// Strict modes (verified, following) — actually filter
filtered = filterByTrustLevel(filtered, trustFilter, getTrustScore);
}
} }
// Apply sorting // Trust tier (1 = trusted, 0 = not) used as the primary sort key in soft mode.
const trustTier = (pubkey: string) => ((getTrustScore(pubkey) ?? -1) >= 60 ? 1 : 0);
// Apply sorting. In soft-trust mode, trusted content sorts first and the
// chosen `sortBy` becomes the tiebreaker WITHIN each trust tier.
filtered.sort((a, b) => { filtered.sort((a, b) => {
if (softTrust && trustScores) {
const tierDiff = trustTier(b.pubkey) - trustTier(a.pubkey);
if (tierDiff !== 0) return tierDiff;
}
switch (sortBy) { switch (sortBy) {
case 'trust': { case 'trust': {
const scoreA = getTrustScore(a.pubkey) || 0; const scoreA = getTrustScore(a.pubkey) || 0;