From dd437693ab525856c243c77cba814af35499d0d1 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 11:21:40 +0700 Subject: [PATCH] Lab offer UX: post-publish moment + "Your Lab profiles" quick access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - After publishing, land with ?published=1 and show a one-time "You're live — share it" banner on the profile (with a Share button), dismissible. - /lab gains a "Your Lab profiles" strip listing the viewer's own profiles, each deep-linking to view/edit — so multi-craft providers aren't hunting through the list. --- src/components/CraftProfileForm.tsx | 2 +- src/i18n/en.ts | 3 +++ src/i18n/es.ts | 3 +++ src/pages/BecomeBuilderPage.tsx | 2 +- src/pages/BecomeCoachPage.tsx | 2 +- src/pages/BecomeEditorPage.tsx | 2 +- src/pages/BecomeFilmerPage.tsx | 2 +- src/pages/BecomePhotographerPage.tsx | 2 +- src/pages/CoachDetailPage.tsx | 31 +++++++++++++++++++++++++++- src/pages/LabPage.tsx | 24 +++++++++++++++++++++ 10 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/components/CraftProfileForm.tsx b/src/components/CraftProfileForm.tsx index 61742df..183b5e5 100644 --- a/src/components/CraftProfileForm.tsx +++ b/src/components/CraftProfileForm.tsx @@ -308,7 +308,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { await createEvent({ kind, content: bio.trim(), tags }); toast({ title: t(existingProfile ? 'labUi.craftForm.updated' : 'labUi.craftForm.created'), description: t('labUi.become.profilePublished') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 90f18ac..c31fc20 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -1994,6 +1994,7 @@ labUi: { offerGraffiti: 'Offer Graffiti', offerPhotography: 'Offer Photography', offerServices: 'Offer Services', + yourProfiles: 'Your Lab profiles', offerTattoo: 'Offer Tattoo', offerTraining: 'Offer Training / Physio', organizeEvents: 'Organize Events', @@ -2598,6 +2599,8 @@ labUi: { manage: 'Manage', admin: 'Admin', editProfile: 'Edit profile', + publishedTitle: "You're live", + publishedBody: 'Your profile is published to The Lab. Share it so skaters can find you.', manageRemove: 'Remove your {{noun}} profile from The Lab.', adminTakeDown: 'Take down this {{noun}} profile. It will be hidden across SkateSpots.', entityLabel: '{{noun}} profile', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 13eab00..e2d79d5 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -1994,6 +1994,7 @@ labUi: { offerGraffiti: 'Ofrecer graffiti', offerPhotography: 'Ofrecer fotografía', offerServices: 'Ofrecer servicios', + yourProfiles: 'Tus perfiles del Lab', offerTattoo: 'Ofrecer tatuajes', offerTraining: 'Ofrecer entrenamiento / fisio', organizeEvents: 'Organizar eventos', @@ -2598,6 +2599,8 @@ labUi: { manage: 'Gestionar', admin: 'Admin', editProfile: 'Editar perfil', + publishedTitle: 'Ya estás visible', + publishedBody: 'Tu perfil está publicado en The Lab. Compártelo para que los skaters te encuentren.', manageRemove: 'Elimina tu perfil de {{noun}} de The Lab.', adminTakeDown: 'Retira este perfil de {{noun}}. Quedará oculto en todo SkateSpots.', entityLabel: 'perfil de {{noun}}', diff --git a/src/pages/BecomeBuilderPage.tsx b/src/pages/BecomeBuilderPage.tsx index fab3a35..792725f 100644 --- a/src/pages/BecomeBuilderPage.tsx +++ b/src/pages/BecomeBuilderPage.tsx @@ -324,7 +324,7 @@ export default function BecomeBuilderPage() { }); toast({ title: existingProfile ? t('labUi.builderForm.updated') : t('labUi.builderForm.created'), description: t('labUi.become.profilePublished') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/pages/BecomeCoachPage.tsx b/src/pages/BecomeCoachPage.tsx index 4f352e5..871c56c 100644 --- a/src/pages/BecomeCoachPage.tsx +++ b/src/pages/BecomeCoachPage.tsx @@ -393,7 +393,7 @@ export default function BecomeCoachPage() { }); toast({ title: existingProfile ? t('labUi.coachForm.updated') : t('labUi.coachForm.created'), description: t('labUi.become.profilePublished') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/pages/BecomeEditorPage.tsx b/src/pages/BecomeEditorPage.tsx index a46dc0e..3b64d90 100644 --- a/src/pages/BecomeEditorPage.tsx +++ b/src/pages/BecomeEditorPage.tsx @@ -249,7 +249,7 @@ export default function BecomeEditorPage() { await createEvent({ kind: SKATE_EDITOR_KIND, content: bio.trim(), tags }); toast({ title: existingProfile ? t('labUi.editorForm.updated') : t('labUi.editorForm.created'), description: t('labUi.editorForm.published') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (err) { toast({ title: t('labUi.become.failedToPublish'), description: err instanceof Error ? err.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/pages/BecomeFilmerPage.tsx b/src/pages/BecomeFilmerPage.tsx index b0e7307..0365d18 100644 --- a/src/pages/BecomeFilmerPage.tsx +++ b/src/pages/BecomeFilmerPage.tsx @@ -281,7 +281,7 @@ export default function BecomeFilmerPage() { }); toast({ title: existingProfile ? t('labUi.filmerForm.updated') : t('labUi.filmerForm.created'), description: t('labUi.become.profilePublished') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/pages/BecomePhotographerPage.tsx b/src/pages/BecomePhotographerPage.tsx index 6ce0654..02dfbe9 100644 --- a/src/pages/BecomePhotographerPage.tsx +++ b/src/pages/BecomePhotographerPage.tsx @@ -348,7 +348,7 @@ export default function BecomePhotographerPage() { }); toast({ title: existingProfile ? t('labUi.photoForm.updated') : t('labUi.photoForm.created'), description: t('labUi.become.profilePublished') }); - navigate(`/coach/${user?.pubkey}/${identifier}`); + navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } diff --git a/src/pages/CoachDetailPage.tsx b/src/pages/CoachDetailPage.tsx index 5b3a51a..af957b4 100644 --- a/src/pages/CoachDetailPage.tsx +++ b/src/pages/CoachDetailPage.tsx @@ -1,7 +1,7 @@ import { dateLocaleCode } from '@/lib/dateFnsI18n'; import { useState } from 'react'; import { useSeoMeta } from '@unhead/react'; -import { useParams, Link, useNavigate } from 'react-router-dom'; +import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'; import { PageError } from '@/components/ErrorState'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -75,6 +75,8 @@ import { ShieldCheck, Share2, Pencil, + PartyPopper, + X, } from 'lucide-react'; import { nip19 } from 'nostr-tools'; @@ -115,6 +117,8 @@ export default function CoachDetailPage() { const { user } = useCurrentUser(); const navigate = useNavigate(); const [shareOpen, setShareOpen] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + const justPublished = searchParams.get('published') === '1'; // Gigs — only relevant for musicians. The hook is disabled for other types. const { data: gigs = [] } = useGigs( @@ -321,6 +325,31 @@ export default function CoachDetailPage() { {t('labUi.become.backToLab')} + {justPublished && user?.pubkey === profile.pubkey && ( +
+ +
+

{t('labUi.detail.publishedTitle')}

+

{t('labUi.detail.publishedBody')}

+
+ + +
+ )} +
{/* Main Content */}
diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx index 092d244..e5bec29 100644 --- a/src/pages/LabPage.tsx +++ b/src/pages/LabPage.tsx @@ -118,6 +118,7 @@ import { ArrowRight, Tent, Sparkles, + Pencil, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { EmptyState as SharedEmptyState } from '@/components/EmptyState'; @@ -1358,6 +1359,10 @@ export default function LabPage() { const { user } = useCurrentUser(); const { data: profiles, isLoading } = useLabProfiles(100); + const ownProfiles = useMemo( + () => (profiles ?? []).filter((p) => p.pubkey === user?.pubkey), + [profiles, user], + ); // Allow deep-linking to a specific craft, e.g. /lab?craft=filming — used by // the community stats cards so tapping a number lands on that craft's list. @@ -1629,6 +1634,25 @@ export default function LabPage() { onClearAll={handleClearAll} /> + {/* ── Your Lab profiles (owner quick access) ── */} + {user && ownProfiles.length > 0 && ( +
+

{t('labUi.yourProfiles')}

+
+ {ownProfiles.map((p) => ( + + + {p.title} + + ))} +
+
+ )} + {/* ── Results grid ── */}
{isLoading ? (