SkateSpots/src/hooks/useCrews.ts
shakespeare.diy 433f59415b Enrich profiles (crews, bot, CLINK), best-in-class New Message picker, map toolbar declutter
Profile page (NIP19Page):
- Add "member of crew X" summary under the header (with founder crown)
- Add a full Crews sidebar card listing every crew the user founds/belongs to
- Add crews count to the Contributions stats (now Spots/Shops/Crews)
- Show a Bot badge for kind 0 bot accounts
- CLINK "Tip in Bitcoin" already surfaces on profiles (from prior work)
- New useUserCrews(pubkey) hook powering the above

Messages:
- Replace paste-only "New Message" dialog with NewMessageDialog: a searchable
  people picker (crew-mates + following, grouped, name-searchable) that also
  accepts a pasted npub/hex as a fallback. Starting a DM now feels like a
  real contacts screen.

Map toolbar consistency:
- Consolidate secondary add actions (Shop / Gear / Tour) into a single
  "+ More" popover; keep primary "Add Spot" prominent. 4 buttons -> 2 controls.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-30 21:36:42 +07:00

152 lines
4.2 KiB
TypeScript

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';
export function useCrews(limit = 200) {
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
return useQuery({
queryKey: ['skate-crews', limit, getQueryKeySuffix()],
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<CrewFocus, 'mixed'>, limit = 30) {
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
const focusTag = CREW_FOCUS_TAG[focus];
return useQuery({
queryKey: ['skate-crews-focus', focus, limit, getQueryKeySuffix()],
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();
return useQuery({
queryKey: ['skate-crew', identifier, pubkey, getQueryKeySuffix()],
queryFn: async () => {
const signal = AbortSignal.timeout(10000);
const filter: {
kinds: number[];
'#d': string[];
authors?: string[];
limit: number;
} = {
kinds: [SKATE_CREW_KIND],
'#d': [identifier],
limit: 1,
};
if (pubkey) filter.authors = [pubkey];
const events = await querySkateContent([filter], { signal });
if (events.length === 0) return null;
const mostRecent = events.reduce((latest, current) =>
current.created_at > latest.created_at ? current : latest
);
return parseSkateCrew(mostRecent);
},
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<UserCrewMembership[]>(() => {
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 };
}