From a4b561fa79ab41b1dfff3023a0a3aebd0379d3b8 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 27 Sep 2026 00:25:10 +0700 Subject: [PATCH] feat(lab): coach/filmer/editor/photographer/builder are editable + have a title field Finishes the Lab audit: the 5 bespoke 'become' pages now prefill from your existing profile (edit, not blind overwrite) and expose a 'Headline' field used as the profile title. Also: - submit button shows Update... when editing; success toast says updated - removed the dead ExistingLabProfileNotice (the warning it gave is now moot) Every one of the 12 Lab provider flows (5 bespoke + 7 craft) now edits + titles consistently. Gate: 101 tests, 0 errors/warnings, boot smoke green. --- src/components/ExistingLabProfileNotice.tsx | 60 ---------------- src/pages/BecomeBuilderPage.tsx | 72 +++++++++++++++++-- src/pages/BecomeCoachPage.tsx | 61 +++++++++++++++-- src/pages/BecomeEditorPage.tsx | 60 ++++++++++++++-- src/pages/BecomeFilmerPage.tsx | 67 ++++++++++++++++-- src/pages/BecomePhotographerPage.tsx | 76 +++++++++++++++++++-- 6 files changed, 301 insertions(+), 95 deletions(-) delete mode 100644 src/components/ExistingLabProfileNotice.tsx diff --git a/src/components/ExistingLabProfileNotice.tsx b/src/components/ExistingLabProfileNotice.tsx deleted file mode 100644 index 331ebac..0000000 --- a/src/components/ExistingLabProfileNotice.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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 ( -
-
- -
-

- You already have a {label} profile -

-

- Publishing this form replaces 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. -

- -
-
-
- ); -} diff --git a/src/pages/BecomeBuilderPage.tsx b/src/pages/BecomeBuilderPage.tsx index 835763c..1b46ab2 100644 --- a/src/pages/BecomeBuilderPage.tsx +++ b/src/pages/BecomeBuilderPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, Link } from 'react-router-dom'; import ngeohash from 'ngeohash'; @@ -23,7 +23,6 @@ import { SpotPicker } from '@/components/SpotPicker'; import { NofferField } from '@/components/NofferField'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { EntityTypeField } from '@/components/EntityTypeField'; -import { ExistingLabProfileNotice } from '@/components/ExistingLabProfileNotice'; import { CrewLinkField } from '@/components/CrewLinkField'; import { SKATE_BUILDER_KIND, @@ -39,6 +38,7 @@ import { cn } from '@/lib/utils'; import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Hammer } from 'lucide-react'; import { LabProUpgrade } from '@/components/LabProUpgrade'; import { usePlatformFee } from '@/hooks/usePlatformFee'; +import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile'; const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS); const SKILL_ENTRIES = Object.entries(BUILDER_SKILL_LABELS); @@ -116,6 +116,51 @@ export default function BecomeBuilderPage() { const [crewRef, setCrewRef] = useState(undefined); const [selectedSpots, setSelectedSpots] = useState([]); const [isGettingLocation, setIsGettingLocation] = useState(false); + const [titleInput, setTitleInput] = useState(''); + const [prefilled, setPrefilled] = useState(false); + + // Prefill from an existing profile so this is an edit, not a blind overwrite. + const { data: existingProfile } = useMyLabProfileEvent(SKATE_BUILDER_KIND, 'builder-profile'); + useEffect(() => { + if (prefilled || !existingProfile) return; + setPrefilled(true); + const tag = (n: string) => existingProfile.tags.find(([t]) => t === n)?.[1]; + const tags = (n: string) => existingProfile.tags.filter(([t]) => t === n).map(([, v]) => v); + const keys = (m: Record) => new Set(Object.keys(m)); + const svcK = keys(BUILDER_SERVICE_LABELS); + const skillK = keys(BUILDER_SKILL_LABELS); + const scaleK = keys(BUILDER_SCALE_LABELS); + const sel = tags('t').filter((v) => v !== 'skatebuilder'); + setTitleInput(tag('title') ?? ''); + setBio(existingProfile.content ?? ''); + setLocation(tag('location') ?? ''); + setImage(tag('image') ?? null); + setExperience(tag('experience') ?? ''); + setRate(tag('rate') ?? ''); + setPhone(tag('phone') ?? ''); + setEmail(tag('email') ?? ''); + setWebsite(tag('website') ?? ''); + setPortfolio(tag('portfolio') ?? ''); + setSocial(tag('social') ?? ''); + setNoffer(tag('noffer') ?? ''); + setGear(tag('gear') ?? ''); + setServiceArea(tag('service_area') ?? ''); + setInsured(tag('insured') === 'true'); + setEntityType((tag('entity') as EntityType) ?? 'collective'); + setMembers(tags('p')); + setCrewRef(existingProfile.tags.find(([t, v]) => t === 'a' && v.startsWith('35006:'))?.[1]); + setSelectedServices(sel.filter((v) => svcK.has(v))); + setSelectedSkills(sel.filter((v) => skillK.has(v))); + setSelectedScale(sel.filter((v) => scaleK.has(v))); + setSelectedSpots( + existingProfile.tags + .filter(([t, v]) => t === 'a' && v.startsWith(`${SKATE_SPOT_KIND}:`)) + .map(([, v]) => { + const [, pk, ...rest] = v.split(':'); + return `${pk}:${rest.join(':')}`; + }), + ); + }, [existingProfile, prefilled]); const getDisplayName = () => { if (author.data?.metadata?.name) return author.data.metadata.name; @@ -181,7 +226,7 @@ export default function BecomeBuilderPage() { const tags: string[][] = [ ['d', identifier], - ['title', displayName], + ['title', titleInput.trim() || displayName], ['t', 'skatebuilder'], ['alt', `Skate ramp/park builder: ${displayName}`], ]; @@ -236,7 +281,7 @@ export default function BecomeBuilderPage() { tags, }); - toast({ title: 'Builder profile created!', description: 'Your profile has been published.' }); + toast({ title: `Builder profile ${existingProfile ? 'updated' : 'created'}!`, description: 'Your profile has been published.' }); navigate(`/coach/${user?.pubkey}/${identifier}`); } catch (error) { toast({ title: 'Failed to publish', description: error instanceof Error ? error.message : 'Error', variant: 'destructive' }); @@ -345,8 +390,6 @@ export default function BecomeBuilderPage() { Tell the scene what you build and how you work - -
@@ -355,6 +398,21 @@ export default function BecomeBuilderPage() {
+
+ + setTitleInput(e.target.value)} + placeholder={getDisplayName()} + maxLength={120} + className="bg-gray-800 border-gray-700" + /> +

+ Shown as the title on your Lab card — e.g. “Ramp & Park Builder”. Defaults to your name. +

+
+