import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { SKATE_CREW_KIND, parseSkateCrew, CREW_FOCUS_TAG, type SkateCrew, type CrewFocus, } from '@/lib/types'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useAdminModeration, filterAdminHidden } from '@/hooks/useAdminModeration'; export function useCrews(limit = 200) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); const hidden = useAdminModeration(); return useQuery({ queryKey: ['skate-crews', limit, getQueryKeySuffix()], select: (crews: SkateCrew[]) => filterAdminHidden(crews, SKATE_CREW_KIND, hidden), queryFn: async () => { const signal = AbortSignal.timeout(10000); const events = await querySkateContent( [ { kinds: [SKATE_CREW_KIND], '#t': ['skatecrew'], limit, }, ], { signal } ); const crews: SkateCrew[] = []; for (const event of events) { const crew = parseSkateCrew(event); if (crew) crews.push(crew); } return crews.sort((a, b) => b.createdAt - a.createdAt); }, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, refetchOnMount: true, }); } /** * Query crews that belong to a specific Lab scene (builder / photo / coaching). * Filters at the relay level via the crew-focus `t` tag, so The Lab pages can * show a "Crews in this scene" strip without scanning every crew. */ export function useCrewsByFocus(focus: Exclude | null, limit = 30) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); const focusTag = focus ? CREW_FOCUS_TAG[focus] : ''; const hidden = useAdminModeration(); return useQuery({ queryKey: ['skate-crews-focus', focus, limit, getQueryKeySuffix()], enabled: !!focus, select: (crews: SkateCrew[]) => filterAdminHidden(crews, SKATE_CREW_KIND, hidden), queryFn: async () => { const signal = AbortSignal.timeout(10000); const events = await querySkateContent( [ { kinds: [SKATE_CREW_KIND], '#t': [focusTag], limit, }, ], { signal } ); const crews: SkateCrew[] = []; for (const event of events) { const crew = parseSkateCrew(event); // Double-check focus after parsing (guards against loose relay matches). if (crew && crew.focus === focus) crews.push(crew); } return crews.sort((a, b) => b.createdAt - a.createdAt); }, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, refetchOnMount: true, }); } export function useCrew(identifier: string, pubkey?: string) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); const hidden = useAdminModeration(); return useQuery({ queryKey: ['skate-crew', identifier, pubkey, getQueryKeySuffix()], select: (crew: SkateCrew | null) => crew && hidden.has({ coord: `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}`, id: crew.id }) ? null : crew, queryFn: async () => { const signal = AbortSignal.timeout(10000); const pickMostRecent = (events: { created_at: number }[]) => events.length ? events.reduce((latest, current) => current.created_at > latest.created_at ? current : latest ) : undefined; // 1) Precise addressable lookup: kind + d-tag (+ author when known). const filter: { kinds: number[]; '#d': string[]; authors?: string[]; limit: number; } = { kinds: [SKATE_CREW_KIND], '#d': [identifier], limit: 1, }; if (pubkey) filter.authors = [pubkey]; let events = await querySkateContent([filter], { signal }); // 2) Fallback: some relays don't index the `d` tag reliably (or choke on // combined author + tag filters). If nothing came back and we know the // author, fetch that author's crews and match the identifier locally. if (events.length === 0 && pubkey) { const byAuthor = await querySkateContent( [{ kinds: [SKATE_CREW_KIND], authors: [pubkey], limit: 200 }], { signal } ); events = byAuthor.filter((e) => e.tags.some(([name, value]) => name === 'd' && value === identifier) ); } const mostRecent = pickMostRecent(events); if (!mostRecent) return null; return parseSkateCrew(mostRecent as typeof events[number]); }, enabled: !!identifier, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, refetchOnMount: true, }); } export interface UserCrewMembership { crew: SkateCrew; /** How the user is attached to this crew. */ role: 'founder' | 'member'; } /** * The crews a given pubkey belongs to — as founder or listed member. * * Powers the "Crews" section on a user's public profile. Founder crews sort * first, then member crews, newest first within each group. */ export function useUserCrews(pubkey?: string) { const { data: crews, isLoading, error } = useCrews(200); const memberships = useMemo(() => { if (!pubkey || !crews) return []; return crews .filter((c) => c.pubkey === pubkey || c.members.includes(pubkey)) .map((crew) => ({ crew, role: crew.pubkey === pubkey ? ('founder' as const) : ('member' as const), })) .sort((a, b) => { if (a.role !== b.role) return a.role === 'founder' ? -1 : 1; return b.crew.createdAt - a.crew.createdAt; }); }, [pubkey, crews]); return { memberships, isLoading, error }; }