SkateSpots/src/components/LabLinkField.tsx
shakespeare.diy d8d1ba32c9 Nutzap safety gate + crew↔Lab cross-linking
Nutzap:
- Gate sending behind NUTZAP_SENDING_ENABLED=false with clear docs; sending
  would require P2PK-locking proofs (fund-loss risk if wrong), so it's disabled
  until implementable/auditable. Receiving + reading unchanged. No dead-end
  button exists (hook was never wired to UI).

Cross-linking crews ↔ Lab collectives:
- types: parseAddrCoord/parseCrewRef/parseLabRefs helpers + LAB_KINDS; add
  crewRef to all 5 Lab interfaces/parsers and labRefs[] to SkateCrew
- CrewLinkField (pick one of your own crews) added to all 5 Become-* forms,
  shown only for collectives; publishes an a-tag on the Lab profile
- LabLinkField (pick your own Lab profiles) added to CrewForm; publishes a-tags
- CrossLinkCards: LinkedCrewCard on Lab detail ("Part of a Crew"), LinkedLabCard
  on crew detail ("Hire This Crew"), resolving coordinates via useCrew/useLabProfile

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 14:27:51 +07:00

91 lines
3.3 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',
};
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.
</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>
);
}