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:
parent
9d1a6999ea
commit
dd437693ab
10 changed files with 66 additions and 7 deletions
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}}',
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue