diff --git a/NIP.md b/NIP.md index 0a1a2cf..18e1ddb 100644 --- a/NIP.md +++ b/NIP.md @@ -1890,17 +1890,17 @@ bare `p` tag on the crew is only a claim until the member self-labels. | `location` | Human-readable home location | | `founded` | Year founded (cosmetic) | | `t: ` | Style/focus tags. Preset vibes plus any user-defined custom hashtag (lowercased, hyphenated slug). Examples: `street`, `transition`, `park`, `diy`, `oldschool`, `allterrain`, `cruising`, `girls`, `youth`, `adaptive`, `downhill`, `night-sessions`, etc. | -| `t: crew-focus-` | Optional **Lab scene** for the crew, surfacing it inside The Lab's matching category. One of `crew-focus-builder` (DIY & Builders), `crew-focus-photo` (Photo & Film — covers filming, photography & editing), or `crew-focus-coaching`. Absent = a "Mixed / General" crew that only appears on the Crews page. Queryable at the relay level via `#t`. | +| `t: crew-focus-` | Optional **Lab scene** for the crew, surfacing it inside The Lab's matching category. One of `crew-focus-builder` (DIY & Builders), `crew-focus-photo` (Photo & Film — filming, photography & editing), `crew-focus-coaching` (Coaching), `crew-focus-art` (Art & Design — designers/artists, graffiti & tattoo), `crew-focus-music` (Band & Music — musicians), or `crew-focus-wellness` (Training & Physio — trainers). Absent = a "Mixed / General" crew that only appears on the Crews page. Queryable at the relay level via `#t`. | | `t: crew-agent` | Optional marker that the crew has enabled its **AI Crew Agent** ("🐝 Sasquatch"). When present, clients may derive the crew's agent identity (see below) and expose an "Ask Sasquatch" experience for members. Absent = no agent. | | `p` | Member pubkeys (hex). Confirmed only when the member self-attests (see below). An optional 4th element `"admin"` marks the member as a delegated admin (see Governance). | | `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` | 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). For a full band profile (genres, instruments, gigs) a crew should instead create a **Musician** Lab profile (kind `35011`), set its focus to `crew-focus-music`, and cross-link the profile via an `a` tag. | | `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. | +| `looking-for` | Optional **open call** — a Lab craft the crew is hiring for. Repeatable. Slugs: `filmer`, `photographer`, `editor`, `coach`, `builder`, `artist`, `musician`, `members`. Surfaces the crew's needs so matching Lab providers can reach out. | ### Crew-Hosted Events diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index 4ed44b3..44743ae 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -34,14 +34,17 @@ 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, Music } from 'lucide-react'; +import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye } from 'lucide-react'; -const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching']; +const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching', 'art', 'music', 'wellness']; const FOCUS_ICONS: Record = { mixed: Sparkles, builder: HardHat, photo: Camera, coaching: GraduationCap, + art: Palette, + music: Music, + wellness: Dumbbell, }; const VIBE_ENTRIES = Object.entries(CREW_VIBE_LABELS); @@ -670,6 +673,14 @@ export function CrewForm({ existing }: CrewFormProps) { owner of the crew's identity.

+

+ + + Each member controls their own visibility — when they + confirm from their key they can choose Public or Crew-only (hidden from + outsiders). You can't set this for them; it's their call. + +

- {/* Crew Band — many crews double as local bands. Optional. */} + {/* Crew Band — many crews double as local bands. Optional. + Interconnects with the Lab: a serious band can set its Crew Type to + "Band & Music" and link a full Musician profile below. */}
@@ -845,6 +858,28 @@ export function CrewForm({ existing }: CrewFormProps) { />
+ + {/* Interconnect nudge: a band should live in the Lab's Music scene. */} + {bandName.trim() && focus !== 'music' && ( + + )} + +

+ + For a full band profile (genres, instruments, gigs), create a{' '} + Musician in The Lab, then link it in the{' '} + Linked Lab profiles section above. +

{/* Logo */} diff --git a/src/components/LabCrewsStrip.tsx b/src/components/LabCrewsStrip.tsx index e434deb..ac339a7 100644 --- a/src/components/LabCrewsStrip.tsx +++ b/src/components/LabCrewsStrip.tsx @@ -2,10 +2,33 @@ import { Link } from 'react-router-dom'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Users, ChevronRight, MapPin } from 'lucide-react'; import { useCrewsByFocus } from '@/hooks/useCrews'; -import { CREW_FOCUS_LABELS, crewFocusForServiceType, type SkateCrew } from '@/lib/types'; +import { CREW_FOCUS_LABELS, crewFocusForServiceType, type SkateCrew, type CrewFocus } from '@/lib/types'; import { Skeleton } from '@/components/ui/skeleton'; -type ServiceType = 'coaching' | 'filming' | 'editing' | 'photography' | 'building'; +/** The Lab page's own ServiceType union (kept in sync with LabPage). */ +type ServiceType = + | 'coaching' | 'filming' | 'editing' | 'photography' | 'building' + | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music'; + +/** Translate the Lab page's ServiceType into a crew focus scene. */ +function focusForLabService(serviceType: ServiceType): Exclude { + switch (serviceType) { + case 'art': + case 'graffiti': + case 'tattoo': + return 'art'; + case 'music': + return 'music'; + case 'training': + return 'wellness'; + case 'building': + return crewFocusForServiceType('building'); + case 'coaching': + return crewFocusForServiceType('coaching'); + default: + return crewFocusForServiceType('filming'); // filming / editing / photography → photo + } +} function CrewChip({ crew }: { crew: SkateCrew }) { return ( @@ -44,7 +67,7 @@ function CrewChip({ crew }: { crew: SkateCrew }) { * cross-reference each other. Renders nothing when there are no matching crews. */ export function LabCrewsStrip({ serviceType }: { serviceType: ServiceType }) { - const focus = crewFocusForServiceType(serviceType); + const focus = focusForLabService(serviceType); const { data: crews, isLoading } = useCrewsByFocus(focus, 20); // Only render the section when there's something to show (or while loading). diff --git a/src/lib/types.ts b/src/lib/types.ts index e8528b2..08ed783 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1252,13 +1252,23 @@ export const CREW_VIBE_LABELS: Record = { * Stored on the crew event as a single-letter, relay-queryable `t` tag using * the `CREW_FOCUS_TAG` map (e.g. `["t", "crew-focus-builder"]`). */ -export type CrewFocus = 'mixed' | 'builder' | 'photo' | 'coaching'; +export type CrewFocus = + | 'mixed' + | 'builder' + | 'photo' + | 'coaching' + | 'art' + | 'music' + | 'wellness'; export const CREW_FOCUS_LABELS: Record = { mixed: 'Mixed / General', builder: 'DIY & Builders', photo: 'Photo & Film', coaching: 'Coaching', + art: 'Art & Design', + music: 'Band & Music', + wellness: 'Training & Physio', }; export const CREW_FOCUS_DESCRIPTIONS: Record = { @@ -1266,6 +1276,9 @@ export const CREW_FOCUS_DESCRIPTIONS: Record = { builder: 'A DIY / park-building crew — also shows in The Lab under Builders.', photo: 'A filming, photography or editing crew — also shows in The Lab under Photo & Film.', coaching: 'A coaching / lessons crew — also shows in The Lab under Coaching.', + art: 'An art, design, graffiti or tattoo collective — also shows in The Lab under Art & Design.', + music: 'A band or music collective — also shows in The Lab under Music.', + wellness: 'A training, fitness or physio crew — also shows in The Lab under Training.', }; /** `t`-tag value stored on the crew event for each non-mixed focus. */ @@ -1273,6 +1286,9 @@ export const CREW_FOCUS_TAG: Record, string> = { builder: 'crew-focus-builder', photo: 'crew-focus-photo', coaching: 'crew-focus-coaching', + art: 'crew-focus-art', + music: 'crew-focus-music', + wellness: 'crew-focus-wellness', }; /** Reverse lookup: `t`-tag value → CrewFocus. */ @@ -1280,6 +1296,9 @@ export const CREW_FOCUS_FROM_TAG: Record = { 'crew-focus-builder': 'builder', 'crew-focus-photo': 'photo', 'crew-focus-coaching': 'coaching', + 'crew-focus-art': 'art', + 'crew-focus-music': 'music', + 'crew-focus-wellness': 'wellness', }; /** @@ -1293,6 +1312,8 @@ export const CREW_LOOKING_FOR_OPTIONS = [ { slug: 'editor', label: 'Video Editor', emoji: '✂️' }, { slug: 'coach', label: 'Coach', emoji: '🎓' }, { slug: 'builder', label: 'Ramp Builder', emoji: '🔨' }, + { slug: 'artist', label: 'Artist / Designer', emoji: '🎨' }, + { slug: 'musician', label: 'Musician / Band', emoji: '🎵' }, { slug: 'members', label: 'New Members', emoji: '🛹' }, ] as const; @@ -1304,13 +1325,39 @@ export const CREW_LOOKING_FOR_LABELS: Record { - if (serviceType === 'building') return 'builder'; - if (serviceType === 'coaching') return 'coaching'; - return 'photo'; // filming, editing, photography + switch (serviceType) { + case 'building': + return 'builder'; + case 'coaching': + return 'coaching'; + case 'artist': + case 'graffiti': + case 'tattoo': + return 'art'; + case 'musician': + return 'music'; + case 'trainer': + return 'wellness'; + default: + return 'photo'; // filming, editing, photography + } } export interface SkateCrew { diff --git a/src/pages/CrewsPage.tsx b/src/pages/CrewsPage.tsx index cb335b9..fc1e2e2 100644 --- a/src/pages/CrewsPage.tsx +++ b/src/pages/CrewsPage.tsx @@ -14,13 +14,16 @@ 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, Music } from 'lucide-react'; +import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2, Music, Palette, Dumbbell } from 'lucide-react'; const FOCUS_FILTERS: { key: CrewFocus | 'all'; label: string; icon: typeof Users }[] = [ { key: 'all', label: 'All', icon: Users }, { key: 'builder', label: CREW_FOCUS_LABELS.builder, icon: HardHat }, { key: 'photo', label: CREW_FOCUS_LABELS.photo, icon: Camera }, { key: 'coaching', label: CREW_FOCUS_LABELS.coaching, icon: GraduationCap }, + { key: 'art', label: CREW_FOCUS_LABELS.art, icon: Palette }, + { key: 'music', label: CREW_FOCUS_LABELS.music, icon: Music }, + { key: 'wellness', label: CREW_FOCUS_LABELS.wellness, icon: Dumbbell }, { key: 'mixed', label: CREW_FOCUS_LABELS.mixed, icon: Sparkles }, ]; @@ -29,6 +32,9 @@ const FOCUS_BADGE_ICONS: Record = { builder: HardHat, photo: Camera, coaching: GraduationCap, + art: Palette, + music: Music, + wellness: Dumbbell, }; function CrewCard({