diff --git a/NIP.md b/NIP.md index 797d6ac..3b12a24 100644 --- a/NIP.md +++ b/NIP.md @@ -1751,6 +1751,8 @@ bare `p` tag on the crew is only a claim until the member self-labels. | `a` | Home spots the crew claims/haunts — references to kind `30818` skate spots (up to 8). Cross-links the crew to the map. | | `nip05` | Optional crew NIP-05 identifier (`local@domain`). Verified against the **founder** pubkey — see NIP-05 Verification. | | `website`, `instagram`, `social` | Links | +| `band` | Optional name of the crew's **band / music project**. Skate scenes and local music are tightly linked, so a crew may double as a band. Freeform text (≤100 chars). | +| `band-url` | Optional link to the crew band's music (Spotify, Bandcamp, YouTube, a Nostr music profile, etc.). Must be an `http(s)` URL. Only meaningful alongside `band`. | | `noffer` | Optional shared tip jar (CLINK Offer — see below) | | `goal` | Optional **crew treasury** — the event id of a NIP-75 Zap Goal (kind `9041`) the crew is raising funds for. See Crew Treasury below. | | `looking-for` | Optional **open call** — a Lab craft the crew is hiring for. Repeatable. Slugs: `filmer`, `photographer`, `editor`, `coach`, `builder`, `members`. Surfaces the crew's needs so matching Lab providers can reach out. | diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index 1ce51f2..a5f9469 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -33,7 +33,7 @@ import { import { useCrewGoal } from '@/hooks/useCrewGoal'; import { useAppContext } from '@/hooks/useAppContext'; import { cn } from '@/lib/utils'; -import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone } from 'lucide-react'; +import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music } from 'lucide-react'; const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching']; const FOCUS_ICONS: Record = { @@ -132,6 +132,8 @@ export function CrewForm({ existing }: CrewFormProps) { const [image, setImage] = useState(existing?.image ?? null); const [website, setWebsite] = useState(existing?.website ?? ''); const [instagram, setInstagram] = useState(existing?.instagram ?? ''); + const [bandName, setBandName] = useState(existing?.bandName ?? ''); + const [bandUrl, setBandUrl] = useState(existing?.bandUrl ?? ''); const [socials, setSocials] = useState(existing?.socials ?? []); const [socialInput, setSocialInput] = useState(''); const [nip05, setNip05] = useState(existing?.nip05 ?? ''); @@ -343,6 +345,10 @@ export function CrewForm({ existing }: CrewFormProps) { if (image) tags.push(['image', image]); if (website.trim()) tags.push(['website', website.trim()]); if (instagram.trim()) tags.push(['instagram', instagram.trim()]); + if (bandName.trim()) tags.push(['band', bandName.trim()]); + if (bandUrl.trim() && /^https?:\/\//i.test(bandUrl.trim())) { + tags.push(['band-url', bandUrl.trim()]); + } for (const s of socials) { if (s.trim()) tags.push(['social', s.trim()]); } @@ -801,6 +807,43 @@ export function CrewForm({ existing }: CrewFormProps) { + {/* Crew Band — many crews double as local bands. Optional. */} +
+
+ + +
+

+ Got a band? Skate scenes and local music go hand in hand — drop your band name and a link to the tunes. +

+
+
+ + setBandName(e.target.value)} + maxLength={100} + className="bg-gray-800 border-gray-700" + /> +
+
+ + setBandUrl(e.target.value)} + spellCheck={false} + autoCapitalize="none" + className="bg-gray-800 border-gray-700" + /> +
+
+
+ {/* Logo */}
diff --git a/src/lib/types.ts b/src/lib/types.ts index 99b0b0e..873c0a7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1033,6 +1033,8 @@ export interface SkateCrew { founded?: string; // Year founded (cosmetic) website?: string; instagram?: string; + bandName?: string; // Crew's skate band / music project name (`band` tag) + bandUrl?: string; // Link to the band's music (Spotify/Bandcamp/YT/Nostr) (`band-url` tag) social?: string; // First social link (backward compat) socials: string[]; // All additional social links (`social` tags) nip05?: string; // Crew's NIP-05 identifier (verified against founder pubkey) @@ -1570,6 +1572,10 @@ export function parseSkateCrew(event: NostrEvent): SkateCrew | null { const founded = event.tags.find(([name]) => name === 'founded')?.[1]; const website = event.tags.find(([name]) => name === 'website')?.[1]; const instagram = event.tags.find(([name]) => name === 'instagram')?.[1]; + // Optional crew band / music project — many crews double as local bands. + const bandName = clampStr(event.tags.find(([name]) => name === 'band')?.[1], 100); + const bandUrlRaw = event.tags.find(([name]) => name === 'band-url')?.[1]?.trim(); + const bandUrl = bandUrlRaw && /^https?:\/\//i.test(bandUrlRaw) ? bandUrlRaw : undefined; const socials = event.tags .filter(([name, value]) => name === 'social' && !!value?.trim()) .map(([, value]) => value.trim()); @@ -1606,6 +1612,8 @@ export function parseSkateCrew(event: NostrEvent): SkateCrew | null { founded, website, instagram, + bandName, + bandUrl, social, socials, nip05, diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx index 228e107..6c2f36f 100644 --- a/src/pages/CrewDetailPage.tsx +++ b/src/pages/CrewDetailPage.tsx @@ -49,7 +49,7 @@ import { SKATE_CREW_KIND, SKATE_SPOT_KIND, CREW_VIBE_LABELS } from '@/lib/types' import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl'; import { cn } from '@/lib/utils'; import NotFound from './NotFound'; -import { Users, MapPin, ArrowLeft, Calendar, Globe, Instagram, Edit, MessageSquare, Zap, ShieldCheck, EyeOff } from 'lucide-react'; +import { Users, MapPin, ArrowLeft, Calendar, Globe, Instagram, Edit, MessageSquare, Zap, ShieldCheck, EyeOff, Music } from 'lucide-react'; import type { NostrEvent } from '@nostrify/nostrify'; function FeedItem({ @@ -504,6 +504,33 @@ export default function CrewDetailPage() { ); })} + {/* Crew band / music project */} + {crew.bandName && ( + crew.bandUrl && sanitizeUrl(crew.bandUrl) ? ( + + + + Band · + {crew.bandName} + + + ) : ( +
+ + + Band · + {crew.bandName} + +
+ ) + )} +
{/* Zap the whole crew — split across founder + confirmed members */} {crewZapRecipients.length > 1 && ( diff --git a/src/pages/CrewsPage.tsx b/src/pages/CrewsPage.tsx index 9e1dd58..be64bd8 100644 --- a/src/pages/CrewsPage.tsx +++ b/src/pages/CrewsPage.tsx @@ -14,7 +14,7 @@ import { crewAccent, crewDistanceKm, formatDistance, type LatLng } from '@/lib/c import { CREW_VIBE_LABELS, CREW_FOCUS_LABELS, type SkateCrew, type CrewFocus } from '@/lib/types'; import type { CrewActivity } from '@/lib/crewHelpers'; import { cn } from '@/lib/utils'; -import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2 } from 'lucide-react'; +import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2, Music } from 'lucide-react'; const FOCUS_FILTERS: { key: CrewFocus | 'all'; label: string; icon: typeof Users }[] = [ { key: 'all', label: 'All', icon: Users }, @@ -81,6 +81,15 @@ function CrewCard({ ); })()} + {crew.bandName && ( + + + Band + + )}
{(crew.location || distanceKm !== null) && (