diff --git a/src/hooks/useSpotCommunityScore.ts b/src/hooks/useSpotCommunityScore.ts index 3418c67..8809b23 100644 --- a/src/hooks/useSpotCommunityScore.ts +++ b/src/hooks/useSpotCommunityScore.ts @@ -17,7 +17,7 @@ import type { NostrEvent } from '@nostrify/nostrify'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { useBatchActivity } from '@/hooks/useTradeIntegrity'; -import { SKATE_SPOT_KIND } from '@/lib/types'; +import { SKATE_SPOT_KIND, SKATE_CREW_KIND, LAB_KINDS } from '@/lib/types'; import { getSatoshisFromBolt11 } from '@/lib/bolt11'; const PHOTO_KIND = 1063; // NIP-94 file metadata (community photo) @@ -38,6 +38,8 @@ export interface SpotCommunityBreakdown { zapSats: number; collections: number; tagSuggestions: number; + crews: number; + labRefs: number; } export type SpotCommunityLevel = 'unverified' | 'seen' | 'community'; @@ -55,6 +57,8 @@ const WEIGHT = { photo: 2, status: 2, zap: 3, // real money + crew: 3, // a whole crew listing it as home turf + labRef: 2, // a Lab pro (builder/coach/…) linking the spot comment: 1, reaction: 1, collection: 1, @@ -75,11 +79,14 @@ interface AccountSignals { zapSats: number; collections: number; tagSuggestions: number; + crews: number; + labRefs: number; } const emptyBreakdown = (): SpotCommunityBreakdown => ({ skaters: 0, photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, + crews: 0, labRefs: 0, }); function classify(ev: NostrEvent): keyof AccountSignals | null { @@ -91,7 +98,9 @@ function classify(ev: NostrEvent): keyof AccountSignals | null { case REACTION_KIND: return 'reactions'; case ZAP_KIND: return 'zaps'; case COLLECTION_KIND: return 'collections'; - default: return null; + case SKATE_CREW_KIND: return 'crews'; + default: + return (LAB_KINDS as readonly number[]).includes(ev.kind) ? 'labRefs' : null; } } @@ -129,6 +138,8 @@ export function useSpotCommunityScore( { kinds: [ZAP_KIND], '#a': [coord], limit: 100 }, { kinds: [ZAP_KIND], '#e': [spot.id], limit: 100 }, { kinds: [COLLECTION_KIND], '#a': [coord], limit: 100 }, + { kinds: [SKATE_CREW_KIND], '#a': [coord], limit: 50 }, + { kinds: [...LAB_KINDS], '#a': [coord], limit: 100 }, ], { signal }, ); @@ -147,7 +158,7 @@ export function useSpotCommunityScore( const key = classify(ev); if (!key) continue; const acc = map.get(ev.pubkey) ?? { - photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, + photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, crews: 0, labRefs: 0, }; acc[key] += 1; if (key === 'zaps') acc.zapSats += zapSatsFrom(ev); @@ -167,6 +178,8 @@ export function useSpotCommunityScore( b.zapSats += a.zapSats; b.collections += a.collections; b.tagSuggestions += a.tagSuggestions; + b.crews += a.crews; + b.labRefs += a.labRefs; } b.skaters = byAccount.size; return b; @@ -187,7 +200,9 @@ export function useSpotCommunityScore( a.comments * WEIGHT.comment + a.reactions * WEIGHT.reaction + a.collections * WEIGHT.collection + - a.tagSuggestions * WEIGHT.tagSuggestion; + a.tagSuggestions * WEIGHT.tagSuggestion + + a.crews * WEIGHT.crew + + a.labRefs * WEIGHT.labRef; const capped = Math.min(raw, PER_ACCOUNT_CAP); // Trust (0..100) and activity both raise the multiplier; unknown → neutral 1. const trustFactor = trust ? 0.4 + 0.6 * ((trust.get(pk) ?? 0) / 100) : 1; @@ -199,7 +214,7 @@ export function useSpotCommunityScore( const level: SpotCommunityLevel = useMemo(() => { if (breakdown.skaters === 0) return 'unverified'; - const strong = breakdown.zaps > 0; + const strong = breakdown.zaps > 0 || breakdown.crews > 0; if ((breakdown.skaters >= 2 && score >= COMMUNITY_THRESHOLD) || strong) return 'community'; return 'seen'; }, [breakdown, score]); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index d7c49ac..8159798 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -1138,6 +1138,11 @@ spotUi: { breakdownCollections: '{{count}} save(s)', breakdownZaps: '{{count}} zap(s)', breakdownTags: '{{count}} tag suggestion(s)', + breakdownCrews: '{{count}} crew(s)', + breakdownLab: '{{count}} pro link(s)', + verifyNudgeTitle: 'Be the first to confirm this spot', + verifyNudgeBody: "A photo or an “I've skated here” is how a spot gets verified — help the next skater out.", + verifyNudgeStatus: "I've skated here", noGearListedYet: 'No gear listed yet', noMessagesYet: 'No messages yet in this local channel. Say hi — "anyone skating tonight?"', noPhoto: 'No photo', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index a3905d5..b570e8d 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -1138,6 +1138,11 @@ spotUi: { breakdownCollections: '{{count}} guardado(s)', breakdownZaps: '{{count}} zap(s)', breakdownTags: '{{count}} etiqueta(s) sugerida(s)', + breakdownCrews: '{{count}} cuadrilla(s)', + breakdownLab: '{{count}} enlace(s) pro', + verifyNudgeTitle: 'Sé el primero en confirmar este spot', + verifyNudgeBody: 'Una foto o un «he patinado aquí» es como se verifica un spot: ayuda al siguiente skater.', + verifyNudgeStatus: 'He patinado aquí', noGearListedYet: 'Aún no hay material publicado', noMessagesYet: 'Aún no hay mensajes en este canal local. Saluda: "¿alguien patina esta noche?"', noPhoto: 'Sin foto', diff --git a/src/pages/SpotDetailPage.tsx b/src/pages/SpotDetailPage.tsx index 406a896..db9eba0 100644 --- a/src/pages/SpotDetailPage.tsx +++ b/src/pages/SpotDetailPage.tsx @@ -197,6 +197,8 @@ export default function SpotDetailPage() { if (b.collections) parts.push(t('spotUi.breakdownCollections', { count: b.collections })); if (b.zaps) parts.push(t('spotUi.breakdownZaps', { count: b.zaps })); if (b.tagSuggestions) parts.push(t('spotUi.breakdownTags', { count: b.tagSuggestions })); + if (b.crews) parts.push(t('spotUi.breakdownCrews', { count: b.crews })); + if (b.labRefs) parts.push(t('spotUi.breakdownLab', { count: b.labRefs })); return parts.join(' · '); }, [community.breakdown, t]); @@ -595,6 +597,20 @@ export default function SpotDetailPage() {

)} + {/* Verify nudge — a logged-in non-owner on an unconfirmed spot. */} + {!isShop && user && !isOwner && !communityVerified && !staffVerified && ( +
+

{t('spotUi.verifyNudgeTitle')}

+

{t('spotUi.verifyNudgeBody')}

+
+ + +
+
+ )} + {/* Reported as not a skate spot — visible to everyone, not just admins. */} {notASpotReporters > 0 && (