diff --git a/src/components/SpotCard.tsx b/src/components/SpotCard.tsx
index d2ee8d5..8b9d69a 100644
--- a/src/components/SpotCard.tsx
+++ b/src/components/SpotCard.tsx
@@ -11,10 +11,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { MapPin, Store, Bookmark, MoreVertical, Flag } from 'lucide-react';
+import { MapPin, Store, Bookmark, MoreVertical, Flag, ShieldCheck } from 'lucide-react';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useVenuesWithPasses } from '@/hooks/useRiderCard';
+import type { SpotCommunityMini } from '@/hooks/useBatchSpotCommunityScores';
import { genUserName } from '@/lib/genUserName';
import { ZapButton } from '@/components/ZapButton';
import { TrustBadge } from '@/components/TrustBadge';
@@ -34,9 +35,11 @@ interface SpotCardProps {
spot: SkateSpot;
/** Optional community status, passed in batched from the parent list/map. */
status?: AggregatedSpotStatus | null;
+ /** Optional community confidence, passed in batched from the parent list/map. */
+ community?: SpotCommunityMini;
}
-export function SpotCard({ spot, status }: SpotCardProps) {
+export function SpotCard({ spot, status, community }: SpotCardProps) {
const { t } = useTranslation();
const author = useAuthor(spot.pubkey);
const { user } = useCurrentUser();
@@ -192,6 +195,20 @@ export function SpotCard({ spot, status }: SpotCardProps) {
{/* Official badge — only shown for verified founder */}
+ {/* Community confidence — batched in by the parent list/map. */}
+ {community?.level === 'community' && (
+
+ {t('spotUi.communityChip')}
+
+ )}
+ {community?.level === 'seen' && (
+
+ {t('spotUi.seenChip')}
+
+ )}
e.preventDefault()}>
diff --git a/src/hooks/useBatchSpotCommunityScores.ts b/src/hooks/useBatchSpotCommunityScores.ts
new file mode 100644
index 0000000..23e1693
--- /dev/null
+++ b/src/hooks/useBatchSpotCommunityScores.ts
@@ -0,0 +1,124 @@
+/**
+ * Batch community confidence for many spots (map grid / lists). One relay query
+ * for all coords, then a distinct-account, tier-weighted level per spot.
+ *
+ * ⚠️ Unlike the single-spot `useSpotCommunityScore`, the batch does NOT apply
+ * web-of-trust / activity weighting — resolving that for hundreds of actors
+ * would be its own fan-out. It still counts distinct accounts, drops the
+ * mapper's own signals, and caps each account's contribution.
+ */
+import { useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import type { NostrEvent } from '@nostrify/nostrify';
+
+import { useSkateRelays } from '@/hooks/useSkateRelays';
+import { SKATE_SPOT_KIND, SKATE_CREW_KIND, LAB_KINDS } from '@/lib/types';
+import {
+ WEIGHT,
+ PER_ACCOUNT_CAP,
+ emptyBreakdown,
+ classifySignal,
+ communityLevel,
+ type AccountSignals,
+ type SpotCommunityBreakdown,
+ type SpotCommunityLevel,
+} from '@/hooks/useSpotCommunityScore';
+
+export interface SpotCommunityMini {
+ level: SpotCommunityLevel;
+ breakdown: SpotCommunityBreakdown;
+}
+
+const blankSignals = (): AccountSignals => ({
+ photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, crews: 0, labRefs: 0,
+});
+
+export function useBatchSpotCommunityScores(
+ spots: Array<{ id: string; pubkey: string; identifier: string }>,
+) {
+ const { querySkateContent } = useSkateRelays();
+
+ const refs = useMemo(
+ () => spots.map((s) => `${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`),
+ [spots],
+ );
+ const mapperByRef = useMemo(() => {
+ const m = new Map();
+ for (const s of spots) m.set(`${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`, s.pubkey);
+ return m;
+ }, [spots]);
+
+ return useQuery