SkateSpots/src/components/AdminReviewCard.tsx
SkateSpots 0419c18810 feat(moderation): positive 'verified' verdict + trust/community signals in the review queue
- MODERATION_LABEL_VERIFIED: the positive counterpart to 'hidden'. Both labels
  are fetched in ONE query; useAdminModeration now exposes verifiedCoords/
  verifiedIds/isVerified() with a local mirror (addLocalConfirmed/
  getLocalConfirmed). useAdminVerify() publishes it (shares the publish path
  with useAdminHide).
- Review queue: a human review can now CONFIRM a spot, not just hide it. A
  confirmed spot clears from the queue and gets a 'Verified by SkateSpots' badge
  on the spot page (staff verdict, shown separately from the community badge).
- Two more signals: mapper trust (useBatchTrustScores) and whether any skater has
  filed a status report (useBatchSpotStatus reporterCount) -> 'mapper not in your
  web of trust' / 'no community signals'. The latter only fires alongside another
  flag so a brand-new real spot isn't punished for being new.
- Flag logic moved to src/lib/spotQuality.ts so the page and AdminReviewCard share it.

Verified: the queue lists the airport entry with suspicious name + no photo + no
community signals, shows mapper trust, and offers Confirm/Hide/Not a skate
spot/Look at it.
2026-09-28 00:25:12 +07:00

43 lines
1.7 KiB
TypeScript

import { Link } from 'react-router-dom';
import { useMemo } from 'react';
import { ShieldAlert, ArrowRight } from 'lucide-react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useSkateSpots } from '@/hooks/useSkateSpots';
import { isAdminPubkey } from '@/lib/admins';
import { isFlagged } from '@/lib/spotQuality';
/**
* Admin-only entry point for the review queue. Renders nothing for everyone
* else, so it can sit in Settings → Advanced beside the other owner cards.
*/
export function AdminReviewCard() {
const { user } = useCurrentUser();
const isAdmin = isAdminPubkey(user?.pubkey);
const { data: spots } = useSkateSpots(300);
// Cheap signals only here (no per-spot trust/status queries just to draw a
// badge) — the review page does the deeper pass.
const flaggedCount = useMemo(() => (spots ?? []).filter(isFlagged).length, [spots]);
if (!isAdmin) return null;
return (
<Link
to="/review"
className="block rounded-2xl border border-amber-500/20 bg-white/[0.02] transition-colors hover:border-amber-500/40"
>
<div className="flex items-center gap-3 px-4 py-3.5">
<ShieldAlert className="h-5 w-5 shrink-0 text-amber-400" />
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-amber-400">Review queue</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{flaggedCount > 0
? `${flaggedCount} spot${flaggedCount === 1 ? '' : 's'} trip the junk signals — hide or dismiss.`
: 'Spots that look like test entries or off-topic pins.'}
</p>
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-gray-500" />
</div>
</Link>
);
}