The Lab: warn before a "Become a …" form silently overwrites an existing profile (1.2.28)

Lab provider profiles (coach, filmer, editor, photographer, builder) are
addressable events with a fixed per-type `d` identifier, so re-publishing
REPLACES the existing profile. Unlike BecomeSkaterPage — which pre-fills from
the existing profile so re-submitting edits rather than clobbers — these five
forms start blank and reconstruct nothing. A returning provider who opened the
form again and submitted would silently wipe every field they'd previously set
(bio, rate, spots, contact, links) down to whatever they happened to retype.

Rather than fragile per-page reconstruction of the mixed services/skills tag
sets (high regression risk), added a small honest guardrail:

- New ExistingLabProfileNotice component: appears only when the current user
  already has a profile of that type (looked up via useLabProfile by their
  pubkey + the known identifier). It states plainly that publishing will
  REPLACE the current profile and links to view the existing one first.
- Wired into all five Become* pages with the correct identifier/label.

Also confirmed (not a bug): all five navigate to /coach/:pubkey/:identifier
after publish — CoachDetailPage is a generic Lab-profile renderer that resolves
any type by the distinct `d` identifier, so this is correct despite the name.

Investigation of the shared submit handlers found them otherwise consistent
and well-formed; left untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-07 22:06:49 +07:00
parent 2b3ee73292
commit 1cb7c109e7
7 changed files with 76 additions and 1 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.2.27", "version": "1.2.28",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "scripts": {

View file

@ -0,0 +1,60 @@
import { Link } from 'react-router-dom';
import { AlertTriangle, Eye } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLabProfile } from '@/hooks/useLabProfiles';
/**
* Lab provider profiles (coach, filmer, editor, photographer, builder…) are
* addressable events keyed by a fixed `d` identifier per type
* (e.g. "coach-profile"). That means re-publishing REPLACES the existing one.
*
* The "Become a …" forms start blank and don't reconstruct prior state, so a
* returning provider who submits again would silently wipe every field they'd
* set before (bio, rate, spots, contact…). This notice makes that consequence
* explicit: it appears only when the current user already has a profile of this
* type, warns that submitting will overwrite it, and links them to view it.
*/
export function ExistingLabProfileNotice({
identifier,
label,
}: {
/** The fixed `d` identifier for this Lab profile type, e.g. "coach-profile". */
identifier: string;
/** Human label for the profile type, e.g. "coach". */
label: string;
}) {
const { user } = useCurrentUser();
const { data: existing } = useLabProfile(identifier, user?.pubkey);
if (!user || !existing) return null;
return (
<div className="rounded-lg border-2 border-amber-500/40 bg-amber-500/5 p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 space-y-2">
<p className="text-sm font-bold text-amber-300">
You already have a {label} profile
</p>
<p className="text-xs text-gray-400">
Publishing this form <span className="font-semibold text-amber-300">replaces</span> your
current {label} profile — anything you don't re-enter here will be lost. Review your
existing profile first so you don't drop details you already set.
</p>
<Button
asChild
variant="outline"
size="sm"
className="border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
>
<Link to={`/coach/${user.pubkey}/${identifier}`}>
<Eye className="mr-1.5 h-3.5 w-3.5" />
View current {label} profile
</Link>
</Button>
</div>
</div>
</div>
);
}

View file

@ -22,6 +22,7 @@ import { SpotPicker } from '@/components/SpotPicker';
import { NofferField } from '@/components/NofferField'; import { NofferField } from '@/components/NofferField';
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
import { EntityTypeField } from '@/components/EntityTypeField'; import { EntityTypeField } from '@/components/EntityTypeField';
import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice';
import { CrewLinkField } from '@/components/CrewLinkField'; import { CrewLinkField } from '@/components/CrewLinkField';
import { import {
SKATE_BUILDER_KIND, SKATE_BUILDER_KIND,
@ -354,6 +355,8 @@ export default function BecomeBuilderPage() {
<CardDescription className="text-gray-400">Tell the scene what you build and how you work</CardDescription> <CardDescription className="text-gray-400">Tell the scene what you build and how you work</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<ExistingLabProfileNotice identifier="builder-profile" label="builder" />
<div className="space-y-2"> <div className="space-y-2">
<Label>Your Builder Identity</Label> <Label>Your Builder Identity</Label>
<div className="p-4 rounded-lg border-2 border-amber-500/30 bg-amber-500/5"> <div className="p-4 rounded-lg border-2 border-amber-500/30 bg-amber-500/5">

View file

@ -22,6 +22,7 @@ import { NofferField } from '@/components/NofferField';
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
import { EntityTypeField } from '@/components/EntityTypeField'; import { EntityTypeField } from '@/components/EntityTypeField';
import { CrewLinkField } from '@/components/CrewLinkField'; import { CrewLinkField } from '@/components/CrewLinkField';
import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice';
import { SKATE_COACH_KIND, SKATE_SPOT_KIND, COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types'; import { SKATE_COACH_KIND, SKATE_SPOT_KIND, COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react'; import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react';
@ -313,6 +314,8 @@ export default function BecomeCoachPage() {
<CardDescription className="text-gray-400">Tell students about your experience</CardDescription> <CardDescription className="text-gray-400">Tell students about your experience</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<ExistingLabProfileNotice identifier="coach-profile" label="coach" />
<div className="space-y-2"> <div className="space-y-2">
<Label>Your Coach Identity</Label> <Label>Your Coach Identity</Label>
<div className="p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5"> <div className="p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">

View file

@ -29,6 +29,7 @@ import {
type EntityType, type EntityType,
} from '@/lib/types'; } from '@/lib/types';
import { EntityTypeField } from '@/components/EntityTypeField'; import { EntityTypeField } from '@/components/EntityTypeField';
import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice';
import { CrewLinkField } from '@/components/CrewLinkField'; import { CrewLinkField } from '@/components/CrewLinkField';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor } from 'lucide-react'; import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor } from 'lucide-react';
@ -283,6 +284,8 @@ export default function BecomeEditorPage() {
<CardDescription className="text-gray-400">Tell the community about your editing style and skills</CardDescription> <CardDescription className="text-gray-400">Tell the community about your editing style and skills</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<ExistingLabProfileNotice identifier="editor-profile" label="editor" />
{/* Identity */} {/* Identity */}
<div className="space-y-2"> <div className="space-y-2">
<Label>Your Identity</Label> <Label>Your Identity</Label>

View file

@ -21,6 +21,7 @@ import { SpotPicker } from '@/components/SpotPicker';
import { NofferField } from '@/components/NofferField'; import { NofferField } from '@/components/NofferField';
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
import { EntityTypeField } from '@/components/EntityTypeField'; import { EntityTypeField } from '@/components/EntityTypeField';
import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice';
import { CrewLinkField } from '@/components/CrewLinkField'; import { CrewLinkField } from '@/components/CrewLinkField';
import { SKATE_FILMER_KIND, SKATE_SPOT_KIND, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types'; import { SKATE_FILMER_KIND, SKATE_SPOT_KIND, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -329,6 +330,8 @@ export default function BecomeFilmerPage() {
<CardDescription className="text-gray-400">Tell clients about your filming expertise</CardDescription> <CardDescription className="text-gray-400">Tell clients about your filming expertise</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<ExistingLabProfileNotice identifier="filming-profile" label="filmer" />
<div className="space-y-2"> <div className="space-y-2">
<Label>Your Filmer Identity</Label> <Label>Your Filmer Identity</Label>
<div className="p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5"> <div className="p-4 rounded-lg border-2 border-red-500/30 bg-red-500/5">

View file

@ -21,6 +21,7 @@ import { SpotPicker } from '@/components/SpotPicker';
import { NofferField } from '@/components/NofferField'; import { NofferField } from '@/components/NofferField';
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
import { EntityTypeField } from '@/components/EntityTypeField'; import { EntityTypeField } from '@/components/EntityTypeField';
import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice';
import { CrewLinkField } from '@/components/CrewLinkField'; import { CrewLinkField } from '@/components/CrewLinkField';
import type { EntityType } from '@/lib/types'; import type { EntityType } from '@/lib/types';
import { import {
@ -371,6 +372,8 @@ export default function BecomePhotographerPage() {
<CardDescription className="text-gray-400">Tell riders about your eye, your kit and your services</CardDescription> <CardDescription className="text-gray-400">Tell riders about your eye, your kit and your services</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<ExistingLabProfileNotice identifier="photography-profile" label="photographer" />
<div className="space-y-2"> <div className="space-y-2">
<Label>Your Photographer Identity</Label> <Label>Your Photographer Identity</Label>
<div className="p-4 rounded-lg border-2 border-indigo-500/30 bg-indigo-500/5"> <div className="p-4 rounded-lg border-2 border-indigo-500/30 bg-indigo-500/5">