A full Lab role, mirroring how Event Organizer was added: - kinds: SKATE_BARBER_KIND 35014, self-tag 'skatebarber', BARBER_*_LABELS, SkateBarber + parseSkateBarber (studio, books_open). - craft config at /become/barber (cyan accent, Sparkles icon), Lab tab 'Barbers', filter sections (services/styles/setting), empty state, FAB item. - Lab pipeline: useLabProfiles kind/tag/parser, LabPage ServiceType 'barbering' + ACCENT/maps/counts, CoachDetailPage per-role maps + studio & books-open block, LabLinkField label, growth stats + Community Stats card. - No crew-scene strip for barbers (LabCrewsStrip returns null). - Documented in NIP.md; smoke test now boots /become/barber.
100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Link2, X, GraduationCap } from 'lucide-react';
|
|
import { useLabProfiles } from '@/hooks/useLabProfiles';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const TYPE_LABEL: Record<string, string> = {
|
|
coach: 'Coach',
|
|
filmer: 'Filmer',
|
|
editor: 'Editor',
|
|
photographer: 'Photographer',
|
|
builder: 'Builder',
|
|
artist: 'Artist',
|
|
graffiti: 'Graffiti',
|
|
tattoo: 'Tattoo',
|
|
trainer: 'Trainer',
|
|
musician: 'Musician',
|
|
judge: 'Judge',
|
|
barber: 'Barber',
|
|
};
|
|
|
|
interface LabLinkFieldProps {
|
|
/** Selected Lab profile coordinates (`kind:pubkey:d`). */
|
|
value: string[];
|
|
onChange: (coords: string[]) => void;
|
|
maxLinks?: number;
|
|
}
|
|
|
|
/**
|
|
* Lets a crew owner link their own Lab profiles (studio/coach/builder listings)
|
|
* so a crew and its service listings cross-reference each other.
|
|
*/
|
|
export function LabLinkField({ value, onChange, maxLinks = 8 }: LabLinkFieldProps) {
|
|
const { user } = useCurrentUser();
|
|
const { data: allProfiles } = useLabProfiles(200);
|
|
|
|
// Only your own Lab profiles — a crew can only vouch a link to listings it controls.
|
|
const myProfiles = useMemo(
|
|
() => (allProfiles ?? []).filter((p) => p.pubkey === user?.pubkey),
|
|
[allProfiles, user?.pubkey]
|
|
);
|
|
|
|
const coordFor = (kind: number, pubkey: string, identifier: string) => `${kind}:${pubkey}:${identifier}`;
|
|
|
|
const toggle = (coord: string) => {
|
|
if (value.includes(coord)) {
|
|
onChange(value.filter((c) => c !== coord));
|
|
} else if (value.length < maxLinks) {
|
|
onChange([...value, coord]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
|
|
<div>
|
|
<Label className="flex items-center gap-2 text-teal-400">
|
|
<Link2 className="h-4 w-4" /> Linked Lab Profiles
|
|
</Label>
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
Run a studio, coaching or build service too? Link your Lab listings so people can hire your crew. Optional.
|
|
When the Lab profile also links back to this crew, both show a green{' '}
|
|
<span className="font-bold text-green-400">Verified</span> badge.
|
|
</p>
|
|
</div>
|
|
|
|
{myProfiles.length === 0 ? (
|
|
<p className="text-xs text-gray-500 italic">
|
|
You don't have any Lab profiles yet. Create one in The Lab to link it here.
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{myProfiles.map((p) => {
|
|
const coord = coordFor(p.event.kind, p.pubkey, p.identifier);
|
|
const selected = value.includes(coord);
|
|
return (
|
|
<button
|
|
key={coord}
|
|
type="button"
|
|
onClick={() => toggle(coord)}
|
|
disabled={!selected && value.length >= maxLinks}
|
|
className={cn(
|
|
'inline-flex items-center gap-1.5 rounded-xl border px-3 py-2 text-xs font-bold transition-all active:scale-95 disabled:opacity-40',
|
|
selected
|
|
? 'bg-teal-500 border-transparent text-white'
|
|
: 'border-gray-700 bg-gray-800 text-gray-300 hover:bg-gray-700'
|
|
)}
|
|
>
|
|
<GraduationCap className="h-3.5 w-3.5" />
|
|
<span className="truncate max-w-[10rem]">{p.title}</span>
|
|
<span className="opacity-60">· {TYPE_LABEL[p.profileType] ?? 'Lab'}</span>
|
|
{selected && <X className="h-3 w-3 opacity-80" />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|