Lab offer UX: post-publish moment + "Your Lab profiles" quick access

- 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.
This commit is contained in:
SkateSpots 2026-10-04 11:21:40 +07:00
parent 9d1a6999ea
commit dd437693ab
10 changed files with 66 additions and 7 deletions

View file

@ -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' });
}

View file

@ -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',

View file

@ -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}}',

View file

@ -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' });
}

View file

@ -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' });
}

View file

@ -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' });
}

View file

@ -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' });
}

View file

@ -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' });
}

View file

@ -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')}
</Link>
{justPublished && user?.pubkey === profile.pubkey && (
<div className={`mb-6 flex items-center gap-3 rounded-xl border-2 p-4 ${accent.border} bg-white/[0.03]`}>
<PartyPopper className={`h-6 w-6 flex-shrink-0 ${accent.text}`} />
<div className="min-w-0 flex-1">
<p className="font-black uppercase">{t('labUi.detail.publishedTitle')}</p>
<p className="text-sm text-gray-400">{t('labUi.detail.publishedBody')}</p>
</div>
<Button onClick={() => setShareOpen(true)} className={`flex-shrink-0 gap-1.5 ${accent.bg} font-bold text-white hover:opacity-90`}>
<Share2 className="h-4 w-4" /> {t('common.share')}
</Button>
<button
type="button"
aria-label={t('common.close')}
onClick={() => {
const next = new URLSearchParams(searchParams);
next.delete('published');
setSearchParams(next, { replace: true });
}}
className="flex-shrink-0 rounded-full p-1 text-gray-500 hover:bg-white/10 hover:text-white"
>
<X className="h-4 w-4" />
</button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Main Content */}
<div className="md:col-span-2 space-y-6 min-w-0">

View file

@ -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 && (
<div className="mt-4 rounded-2xl border border-white/10 bg-white/[0.03] p-3">
<p className="mb-2 text-[10px] font-black uppercase tracking-widest text-gray-500">{t('labUi.yourProfiles')}</p>
<div className="flex flex-wrap gap-2">
{ownProfiles.map((p) => (
<Link
key={p.id}
to={`/coach/${p.pubkey}/${p.identifier}`}
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-bold text-gray-200 transition-colors hover:bg-white/10"
>
<Pencil className="h-3 w-3 text-[#ccff00]" />
{p.title}
</Link>
))}
</div>
</div>
)}
{/* ── Results grid ── */}
<div className="mt-5">
{isLoading ? (