SkateSpots/src/components/CrewLinkField.tsx
shakespeare.diy 64b91c0ae2 Mutual crew<->Lab link verification (trust badge) + polish
The crew<->collective links existed on both sides (crew.labRefs, profile.crewRef)
but were unverified one-sided claims. Now:
- CrossLinkCards resolve the other entity and show a green "Verified" badge only
  when BOTH sides link each other (crew lists the Lab profile AND the profile
  lists the crew). One-sided claims show a plain link — never false trust.
- LinkedCrewCard / LinkedLabCard take a selfCoord; CoachDetailPage and
  CrewDetailPage pass their own coordinate so the check works.
- CrewLinkField + LabLinkField now explain the mutual-verified behavior, so the
  feature is discoverable. Both fields already restrict to the user's OWN
  entities, so a verified link genuinely requires one owner to confirm both sides.
- Completed LabLinkField TYPE_LABEL for all crafts (artist/graffiti/tattoo/
  trainer/musician/judge no longer fall back to "Lab").

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:29:57 +07:00

84 lines
3.2 KiB
TypeScript

import { useMemo } from 'react';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Users, X, Link2 } from 'lucide-react';
import { useCrews } from '@/hooks/useCrews';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { SKATE_CREW_KIND } from '@/lib/types';
import { cn } from '@/lib/utils';
interface CrewLinkFieldProps {
/** Selected crew coordinate `35006:pubkey:d`, or undefined. */
value?: string;
onChange: (coord: string | undefined) => void;
accentText?: string;
accentBg?: string;
}
/**
* Lets a Lab-profile owner link ONE of their own crews. Cross-links a collective
* Lab profile (studio) to its crew so each surfaces the other.
*/
export function CrewLinkField({ value, onChange, accentText = 'text-teal-400', accentBg = 'bg-teal-500' }: CrewLinkFieldProps) {
const { user } = useCurrentUser();
const { data: allCrews } = useCrews(200);
// Only offer the crews this user founded — you can only vouch a link from a crew you control.
const myCrews = useMemo(
() => (allCrews ?? []).filter((c) => c.pubkey === user?.pubkey),
[allCrews, user?.pubkey]
);
const coordFor = (pubkey: string, identifier: string) => `${SKATE_CREW_KIND}:${pubkey}:${identifier}`;
return (
<div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
<div>
<Label className={cn('flex items-center gap-2', accentText)}>
<Link2 className="h-4 w-4" /> Linked Crew
</Label>
<p className="mt-1 text-xs text-gray-500">
Run a crew too? Link it so people can jump between your studio and your crew. Optional.
Link this crew back to the profile from the crew editor and it'll show a green{' '}
<span className="font-bold text-green-400">Verified</span> badge on both.
</p>
</div>
{myCrews.length === 0 ? (
<p className="text-xs text-gray-500 italic">
You don't have any crews yet. Create one on the Crews page to link it here.
</p>
) : (
<div className="flex flex-wrap gap-2">
{myCrews.map((crew) => {
const coord = coordFor(crew.pubkey, crew.identifier);
const selected = value === coord;
return (
<button
key={coord}
type="button"
onClick={() => onChange(selected ? undefined : coord)}
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',
selected
? `${accentBg} border-transparent text-white`
: 'border-gray-700 bg-gray-800 text-gray-300 hover:bg-gray-700'
)}
>
<Users className="h-3.5 w-3.5" />
{crew.name}
{selected && <X className="h-3 w-3 opacity-80" />}
</button>
);
})}
</div>
)}
{value && !myCrews.some((c) => coordFor(c.pubkey, c.identifier) === value) && (
<Badge variant="outline" className="gap-1 text-[10px] text-gray-400">
<Users className="h-3 w-3" /> Linked crew ({value.split(':')[2]})
</Badge>
)}
</div>
);
}