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 });
|
await createEvent({ kind, content: bio.trim(), tags });
|
||||||
toast({ title: t(existingProfile ? 'labUi.craftForm.updated' : 'labUi.craftForm.created'), description: t('labUi.become.profilePublished') });
|
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) {
|
} catch (error) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
|
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',
|
offerGraffiti: 'Offer Graffiti',
|
||||||
offerPhotography: 'Offer Photography',
|
offerPhotography: 'Offer Photography',
|
||||||
offerServices: 'Offer Services',
|
offerServices: 'Offer Services',
|
||||||
|
yourProfiles: 'Your Lab profiles',
|
||||||
offerTattoo: 'Offer Tattoo',
|
offerTattoo: 'Offer Tattoo',
|
||||||
offerTraining: 'Offer Training / Physio',
|
offerTraining: 'Offer Training / Physio',
|
||||||
organizeEvents: 'Organize Events',
|
organizeEvents: 'Organize Events',
|
||||||
|
|
@ -2598,6 +2599,8 @@ labUi: {
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
editProfile: 'Edit profile',
|
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.',
|
manageRemove: 'Remove your {{noun}} profile from The Lab.',
|
||||||
adminTakeDown: 'Take down this {{noun}} profile. It will be hidden across SkateSpots.',
|
adminTakeDown: 'Take down this {{noun}} profile. It will be hidden across SkateSpots.',
|
||||||
entityLabel: '{{noun}} profile',
|
entityLabel: '{{noun}} profile',
|
||||||
|
|
|
||||||
|
|
@ -1994,6 +1994,7 @@ labUi: {
|
||||||
offerGraffiti: 'Ofrecer graffiti',
|
offerGraffiti: 'Ofrecer graffiti',
|
||||||
offerPhotography: 'Ofrecer fotografía',
|
offerPhotography: 'Ofrecer fotografía',
|
||||||
offerServices: 'Ofrecer servicios',
|
offerServices: 'Ofrecer servicios',
|
||||||
|
yourProfiles: 'Tus perfiles del Lab',
|
||||||
offerTattoo: 'Ofrecer tatuajes',
|
offerTattoo: 'Ofrecer tatuajes',
|
||||||
offerTraining: 'Ofrecer entrenamiento / fisio',
|
offerTraining: 'Ofrecer entrenamiento / fisio',
|
||||||
organizeEvents: 'Organizar eventos',
|
organizeEvents: 'Organizar eventos',
|
||||||
|
|
@ -2598,6 +2599,8 @@ labUi: {
|
||||||
manage: 'Gestionar',
|
manage: 'Gestionar',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
editProfile: 'Editar perfil',
|
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.',
|
manageRemove: 'Elimina tu perfil de {{noun}} de The Lab.',
|
||||||
adminTakeDown: 'Retira este perfil de {{noun}}. Quedará oculto en todo SkateSpots.',
|
adminTakeDown: 'Retira este perfil de {{noun}}. Quedará oculto en todo SkateSpots.',
|
||||||
entityLabel: 'perfil de {{noun}}',
|
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') });
|
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) {
|
} catch (error) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
|
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') });
|
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) {
|
} catch (error) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
|
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 });
|
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') });
|
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) {
|
} catch (err) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: err instanceof Error ? err.message : t('common.error'), variant: 'destructive' });
|
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') });
|
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) {
|
} catch (error) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
|
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') });
|
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) {
|
} catch (error) {
|
||||||
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
|
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 { dateLocaleCode } from '@/lib/dateFnsI18n';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSeoMeta } from '@unhead/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 { PageError } from '@/components/ErrorState';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
@ -75,6 +75,8 @@ import {
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Share2,
|
Share2,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
PartyPopper,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
|
@ -115,6 +117,8 @@ export default function CoachDetailPage() {
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [shareOpen, setShareOpen] = useState(false);
|
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.
|
// Gigs — only relevant for musicians. The hook is disabled for other types.
|
||||||
const { data: gigs = [] } = useGigs(
|
const { data: gigs = [] } = useGigs(
|
||||||
|
|
@ -321,6 +325,31 @@ export default function CoachDetailPage() {
|
||||||
{t('labUi.become.backToLab')}
|
{t('labUi.become.backToLab')}
|
||||||
</Link>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="md:col-span-2 space-y-6 min-w-0">
|
<div className="md:col-span-2 space-y-6 min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Tent,
|
Tent,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Pencil,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
|
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
|
||||||
|
|
@ -1358,6 +1359,10 @@ export default function LabPage() {
|
||||||
|
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { data: profiles, isLoading } = useLabProfiles(100);
|
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
|
// 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.
|
// the community stats cards so tapping a number lands on that craft's list.
|
||||||
|
|
@ -1629,6 +1634,25 @@ export default function LabPage() {
|
||||||
onClearAll={handleClearAll}
|
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 ── */}
|
{/* ── Results grid ── */}
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue