Crews page
- "Near me" geolocation toggle with haversine distance sort + distance chips
- Crew count + cities social-proof stat header
- Deterministic per-crew vibe-colour accents (no more wall of teal)
- Activity pulse ("🔥 active this week") + OG/Verified/Established rep badges
- Rich empty state ("Claim your local scene") instead of dead "No crews yet"
Crew synergies
- Crew treasury: transparent, non-custodial NIP-75 zap goal (kind 9041) with
live progress bar tallied from zap receipts; funds settle to founder's own
Lightning wallet — no shared wallet or new wallet settings needed
- Crew-hosted events: host a jam/comp as the crew (event a-tag → crew coord),
"Hosted by [crew]" on event pages, Crew Events list on the crew page
- Crew ↔ Lab hiring pipeline: "Open Call / looking-for" flags (filmer, editor,
coach, builder, members) surfaced on the crew page
- Request to join: encrypted NIP-17 DM to founder + admins (no founder-push
bottleneck, no public join event)
- Reverse links: "Crews that skate here" on spot pages
Contacts (0xchat-style + Armada-style privacy)
- New /contacts page with Crew-mates / Follows / Recent DMs / Saved tabs
- Per-contact quick actions: message, zap, view profile, star
- Private encrypted contacts list (NIP-51 kind 30000, NIP-44 encrypted to self)
- Added to burger menu + desktop More menu (gated to logged-in users)
Docs: NIP.md updated with crew treasury, crew-hosted events, looking-for tags,
and the private encrypted contacts list.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import { crewBadge, ACTIVITY_META, type CrewActivity } from '@/lib/crewHelpers';
|
|
import type { SkateCrew } from '@/lib/types';
|
|
|
|
/**
|
|
* A small "legitimacy" badge (OG / Verified / Established) derived from a crew's
|
|
* founding year, NIP-05 and member count. Social proof for the directory.
|
|
*/
|
|
export function CrewRepBadge({
|
|
crew,
|
|
confirmedMembers,
|
|
className,
|
|
}: {
|
|
crew: Pick<SkateCrew, 'founded' | 'nip05' | 'members'>;
|
|
confirmedMembers?: number;
|
|
className?: string;
|
|
}) {
|
|
const badge = crewBadge(crew, confirmedMembers);
|
|
if (badge.key === 'crew') return null;
|
|
|
|
const styles: Record<string, string> = {
|
|
og: 'bg-amber-500/15 text-amber-300 border-amber-500/40',
|
|
verified: 'bg-teal-500/15 text-teal-300 border-teal-500/40',
|
|
established: 'bg-fuchsia-500/15 text-fuchsia-300 border-fuchsia-500/40',
|
|
};
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide',
|
|
styles[badge.key],
|
|
className,
|
|
)}
|
|
title={badge.label}
|
|
>
|
|
<span className="leading-none">{badge.emoji}</span>
|
|
{badge.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A subtle activity pulse ("🔥 Active this week"). Dormant crews render nothing
|
|
* unless `showDormant` is set, so the grid highlights life rather than death.
|
|
*/
|
|
export function CrewActivityPulse({
|
|
activity,
|
|
showDormant = false,
|
|
className,
|
|
}: {
|
|
activity: CrewActivity | undefined;
|
|
showDormant?: boolean;
|
|
className?: string;
|
|
}) {
|
|
if (!activity) return null;
|
|
// In dense card grids we only surface positive signals — hide dormant crews.
|
|
if (activity.level === 'dormant' && !showDormant) return null;
|
|
|
|
const meta = ACTIVITY_META[activity.level];
|
|
const isHot = activity.level === 'hot';
|
|
|
|
return (
|
|
<span
|
|
className={cn('inline-flex items-center gap-1 text-[10px] font-bold', meta.text, className)}
|
|
title={
|
|
activity.upcomingEvents > 0
|
|
? `${activity.upcomingEvents} upcoming event${activity.upcomingEvents === 1 ? '' : 's'}`
|
|
: meta.label
|
|
}
|
|
>
|
|
<span className="relative flex h-1.5 w-1.5">
|
|
{isHot && (
|
|
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', meta.dot)} />
|
|
)}
|
|
<span className={cn('relative inline-flex h-1.5 w-1.5 rounded-full', meta.dot)} />
|
|
</span>
|
|
{activity.upcomingEvents > 0
|
|
? `${activity.upcomingEvents} event${activity.upcomingEvents === 1 ? '' : 's'} coming`
|
|
: meta.label}
|
|
</span>
|
|
);
|
|
}
|