From 64b91c0ae2e074abed2e37d93f34d41e4e054545 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 3 Sep 2026 15:29:57 +0700 Subject: [PATCH] Mutual crew<->Lab link verification (trust badge) + polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/CrewLinkField.tsx | 2 + src/components/CrossLinkCards.tsx | 76 +++++++++++++++++++++++++------ src/components/LabLinkField.tsx | 8 ++++ src/pages/CoachDetailPage.tsx | 8 +++- src/pages/CrewDetailPage.tsx | 5 +- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/components/CrewLinkField.tsx b/src/components/CrewLinkField.tsx index e136d6d..35602cd 100644 --- a/src/components/CrewLinkField.tsx +++ b/src/components/CrewLinkField.tsx @@ -39,6 +39,8 @@ export function CrewLinkField({ value, onChange, accentText = 'text-teal-400', a

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{' '} + Verified badge on both.

diff --git a/src/components/CrossLinkCards.tsx b/src/components/CrossLinkCards.tsx index a994452..610ceab 100644 --- a/src/components/CrossLinkCards.tsx +++ b/src/components/CrossLinkCards.tsx @@ -1,11 +1,33 @@ import { Link } from 'react-router-dom'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Users, GraduationCap, ChevronRight, Link2 } from 'lucide-react'; +import { Users, GraduationCap, ChevronRight, Link2, BadgeCheck } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useCrew } from '@/hooks/useCrews'; import { useLabProfile } from '@/hooks/useLabProfiles'; import { parseAddrCoord } from '@/lib/types'; +/** + * Small "✓ Verified" pill shown when a link is confirmed from BOTH sides + * (the crew links the Lab profile AND the Lab profile links the crew). A + * one-sided claim shows nothing — we never imply trust that isn't mutual. + */ +function MutualBadge() { + return ( + + + + + Verified + + + + Both sides confirm this link — the crew and the Lab profile each point at + the other. + + + ); +} + const LAB_TYPE_LABEL: Record = { 35001: 'Coach', 35002: 'Filmer', @@ -19,13 +41,19 @@ const LAB_TYPE_LABEL: Record = { 35011: 'Musician', }; -/** A single linked-crew row, resolved from a `35006:pubkey:d` coordinate. */ -function CrewRow({ coord }: { coord: string }) { +/** + * A single linked-crew row, resolved from a `35006:pubkey:d` coordinate. + * `selfCoord` is the coordinate of the Lab profile showing this card — used to + * check whether the crew links BACK (mutual = verified). + */ +function CrewRow({ coord, selfCoord }: { coord: string; selfCoord?: string }) { const parsed = parseAddrCoord(coord); const { data: crew } = useCrew(parsed?.identifier ?? '', parsed?.pubkey); if (!parsed) return null; + const mutual = !!selfCoord && !!crew?.labRefs?.includes(selfCoord); + return (
-

{crew?.name ?? 'Skate Crew'}

+
+

{crew?.name ?? 'Skate Crew'}

+ {mutual && } +

Crew{crew?.location ? ` · ${crew.location}` : ''}

@@ -47,14 +78,20 @@ function CrewRow({ coord }: { coord: string }) { ); } -/** A single linked-Lab-profile row, resolved from a `kind:pubkey:d` coordinate. */ -function LabRow({ coord }: { coord: string }) { +/** + * A single linked-Lab-profile row, resolved from a `kind:pubkey:d` coordinate. + * `selfCoord` is the coordinate of the crew showing this card — used to check + * whether the Lab profile links BACK (mutual = verified). + */ +function LabRow({ coord, selfCoord }: { coord: string; selfCoord?: string }) { const parsed = parseAddrCoord(coord); const { data: profile } = useLabProfile(parsed?.identifier ?? '', parsed?.pubkey); if (!parsed) return null; const typeLabel = LAB_TYPE_LABEL[parsed.kind] ?? 'Lab'; + const mutual = !!selfCoord && profile?.crewRef === selfCoord; + return (
-

{profile?.title ?? `${typeLabel} profile`}

+
+

{profile?.title ?? `${typeLabel} profile`}

+ {mutual && } +

In The Lab · {typeLabel}

@@ -76,8 +116,12 @@ function LabRow({ coord }: { coord: string }) { ); } -/** Card shown on a Lab profile: the crew it belongs to. */ -export function LinkedCrewCard({ crewRef }: { crewRef?: string }) { +/** + * Card shown on a Lab profile: the crew it belongs to. + * `selfCoord` is this Lab profile's own coordinate (`kind:pubkey:d`) so the row + * can show a "Verified" badge when the crew links back. + */ +export function LinkedCrewCard({ crewRef, selfCoord }: { crewRef?: string; selfCoord?: string }) { if (!crewRef) return null; return ( @@ -87,14 +131,18 @@ export function LinkedCrewCard({ crewRef }: { crewRef?: string }) { - + ); } -/** Card shown on a crew: the Lab service profiles it links to. */ -export function LinkedLabCard({ labRefs }: { labRefs: string[] }) { +/** + * Card shown on a crew: the Lab service profiles it links to. + * `selfCoord` is the crew's own coordinate (`35006:pubkey:d`) so each row can + * show a "Verified" badge when that Lab profile links back to this crew. + */ +export function LinkedLabCard({ labRefs, selfCoord }: { labRefs: string[]; selfCoord?: string }) { if (!labRefs || labRefs.length === 0) return null; return ( @@ -106,7 +154,7 @@ export function LinkedLabCard({ labRefs }: { labRefs: string[] }) { {labRefs.map((coord) => ( - + ))} diff --git a/src/components/LabLinkField.tsx b/src/components/LabLinkField.tsx index f57e0a8..2914649 100644 --- a/src/components/LabLinkField.tsx +++ b/src/components/LabLinkField.tsx @@ -11,6 +11,12 @@ const TYPE_LABEL: Record = { editor: 'Editor', photographer: 'Photographer', builder: 'Builder', + artist: 'Artist', + graffiti: 'Graffiti', + tattoo: 'Tattoo', + trainer: 'Trainer', + musician: 'Musician', + judge: 'Judge', }; interface LabLinkFieldProps { @@ -52,6 +58,8 @@ export function LabLinkField({ value, onChange, maxLinks = 8 }: LabLinkFieldProp

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{' '} + Verified badge.

diff --git a/src/pages/CoachDetailPage.tsx b/src/pages/CoachDetailPage.tsx index 0e66795..14c8c2a 100644 --- a/src/pages/CoachDetailPage.tsx +++ b/src/pages/CoachDetailPage.tsx @@ -619,8 +619,12 @@ export default function CoachDetailPage() { /> )} - {/* Linked crew (cross-link) */} - + {/* Linked crew (cross-link) — pass this profile's own coord so a + mutual link shows a "Verified" badge. */} + {/* Location Card */} {profile.location && ( diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx index f1bda9d..67cfac3 100644 --- a/src/pages/CrewDetailPage.tsx +++ b/src/pages/CrewDetailPage.tsx @@ -512,8 +512,9 @@ export default function CrewDetailPage() { )} - {/* Linked Lab profiles — hire this crew */} - + {/* Linked Lab profiles — hire this crew. Pass the crew's own coord + so each row shows a "Verified" badge when the Lab profile links back. */} + {/* Home spots */} {crew.spotRefs.length > 0 && (