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 <assistant@shakespeare.diy>
This commit is contained in:
parent
96f7c3192d
commit
6e11aaab7d
5 changed files with 126 additions and 15 deletions
6
NIP.md
6
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: <vibe>` | 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-<scene>` | 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-<scene>` | 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CrewFocus, typeof Users> = {
|
||||
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.
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 flex items-start gap-1 text-xs text-gray-500">
|
||||
<Eye className="mt-0.5 h-3 w-3 shrink-0 text-teal-400" />
|
||||
<span>
|
||||
Each member controls their <strong className="text-teal-300">own visibility</strong> — when they
|
||||
confirm from their key they can choose <em>Public</em> or <em>Crew-only</em> (hidden from
|
||||
outsiders). You can't set this for them; it's their call.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
|
|
@ -810,7 +821,9 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
</div>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="space-y-3 rounded-xl border border-purple-500/20 bg-purple-500/[0.04] p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Music className="h-4 w-4 text-purple-400" />
|
||||
|
|
@ -845,6 +858,28 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interconnect nudge: a band should live in the Lab's Music scene. */}
|
||||
{bandName.trim() && focus !== 'music' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFocus('music')}
|
||||
className="flex w-full items-start gap-2 rounded-lg border border-purple-500/40 bg-purple-500/10 px-3 py-2.5 text-left transition-colors hover:bg-purple-500/20"
|
||||
>
|
||||
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-purple-300" />
|
||||
<span className="text-xs text-purple-200">
|
||||
You've got a band — tap to set your <strong>Crew Type</strong> to{' '}
|
||||
<strong>Band & Music</strong> so you also show up in The Lab's Music scene.
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className="flex items-start gap-1.5 text-[11px] leading-snug text-gray-500">
|
||||
<Music className="mt-0.5 h-3 w-3 shrink-0 text-purple-400" />
|
||||
For a full band profile (genres, instruments, gigs), create a{' '}
|
||||
<strong className="text-gray-300">Musician</strong> in The Lab, then link it in the{' '}
|
||||
<strong className="text-gray-300">Linked Lab profiles</strong> section above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
|
|
|||
|
|
@ -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<CrewFocus, 'mixed'> {
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -1252,13 +1252,23 @@ export const CREW_VIBE_LABELS: Record<string, string> = {
|
|||
* 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<CrewFocus, string> = {
|
||||
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<CrewFocus, string> = {
|
||||
|
|
@ -1266,6 +1276,9 @@ export const CREW_FOCUS_DESCRIPTIONS: Record<CrewFocus, string> = {
|
|||
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<Exclude<CrewFocus, 'mixed'>, 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<string, CrewFocus> = {
|
|||
'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<string, { label: string; emoji: str
|
|||
/** NIP-75 Zap Goal kind — used for crew funding goals / treasury. */
|
||||
export const ZAP_GOAL_KIND = 9041;
|
||||
|
||||
/** Lab service types that map onto a crew scene (used by LabCrewsStrip). */
|
||||
export type LabServiceType =
|
||||
| 'coaching'
|
||||
| 'filming'
|
||||
| 'editing'
|
||||
| 'photography'
|
||||
| 'building'
|
||||
| 'artist'
|
||||
| 'graffiti'
|
||||
| 'tattoo'
|
||||
| 'trainer'
|
||||
| 'musician';
|
||||
|
||||
/** Map a Lab service type to the crew focus that belongs in that scene. */
|
||||
export function crewFocusForServiceType(
|
||||
serviceType: 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
|
||||
serviceType: LabServiceType
|
||||
): Exclude<CrewFocus, 'mixed'> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<CrewFocus, typeof Users> = {
|
|||
builder: HardHat,
|
||||
photo: Camera,
|
||||
coaching: GraduationCap,
|
||||
art: Palette,
|
||||
music: Music,
|
||||
wellness: Dumbbell,
|
||||
};
|
||||
|
||||
function CrewCard({
|
||||
|
|
|
|||
Loading…
Reference in a new issue