From 6e11aaab7dd5c2caa97d7f39970943e0deb96b65 Mon Sep 17 00:00:00 2001
From: "shakespeare.diy"
Date: Mon, 10 Aug 2026 22:14:21 +0700
Subject: [PATCH] Crew form UX: expand Crew Type to 7 scenes (Art, Music,
Wellness) mapping to new Lab crafts; rethink Crew Band with Lab-music
interconnect nudge; surface member visibility to founders; add
Artist/Musician open-call options; update Crews page filters + NIP.md
Co-authored-by: shakespeare.diy
---
NIP.md | 6 ++--
src/components/CrewForm.tsx | 41 +++++++++++++++++++++--
src/components/LabCrewsStrip.tsx | 29 ++++++++++++++--
src/lib/types.ts | 57 +++++++++++++++++++++++++++++---
src/pages/CrewsPage.tsx | 8 ++++-
5 files changed, 126 insertions(+), 15 deletions(-)
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. */}
+
+ {/* 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({