From 9d1a6999eac91ed8e9d63b6f2c347ae7774ff1e7 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 11:16:54 +0700 Subject: [PATCH] Add owner Edit + Share to Lab provider profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The offer → publish loop had two dead ends: a provider's own page only offered Delete (no Edit), and there was no way to share a listing (the whole point of announcing a service). - Owner "Manage" card gains an "Edit profile" button deep-linking to /become/ (prefills the existing profile). Admins still get only take-down, not a misleading self-edit link. - A Share button on every provider page opens ShareProfileDialog (QR + copy + native share) with the service URL /coach//. - ShareProfileDialog accepts an optional `path` to override the default / share URL. - main.tsx clean-link shim now also rewrites /coach// to the hash route, so shared /coach/... links resolve on the live site. Follow-ups (not done): live preview before publishing, draft/autosave for the long forms, a multi-step/progress layout, and a "your Lab profiles" manage strip on /lab. --- src/components/ShareProfileDialog.tsx | 7 +++++-- src/i18n/en.ts | 1 + src/i18n/es.ts | 1 + src/main.tsx | 7 ++++--- src/pages/CoachDetailPage.tsx | 28 +++++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/components/ShareProfileDialog.tsx b/src/components/ShareProfileDialog.tsx index 4251f29..15b6dc8 100644 --- a/src/components/ShareProfileDialog.tsx +++ b/src/components/ShareProfileDialog.tsx @@ -13,6 +13,9 @@ interface ShareProfileDialogProps { npub: string; /** Display name, used in the share sheet + QR caption. */ displayName: string; + /** Optional path (e.g. `/coach//`) appended to the canonical origin + * instead of the default `/` — for sharing a specific Lab profile. */ + path?: string; } /** @@ -21,7 +24,7 @@ interface ShareProfileDialogProps { * than a raw npub. Falls back gracefully to copy-link when the native share * sheet isn't available. */ -export function ShareProfileDialog({ open, onOpenChange, npub, displayName }: ShareProfileDialogProps) { +export function ShareProfileDialog({ open, onOpenChange, npub, displayName, path }: ShareProfileDialogProps) { const { t } = useTranslation(); const { toast } = useToast(); const [qr, setQr] = useState(null); @@ -31,7 +34,7 @@ export function ShareProfileDialog({ open, onOpenChange, npub, displayName }: Sh // real domain — never the preview/sandbox iframe host that window.location // would give us inside the editor. const CANONICAL_ORIGIN = 'https://skate-spots.org'; - const shareUrl = `${CANONICAL_ORIGIN}/${npub}`; + const shareUrl = `${CANONICAL_ORIGIN}${path ?? `/${npub}`}`; useEffect(() => { if (!open) return; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 6b066f5..90f18ac 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2597,6 +2597,7 @@ labUi: { portfolioDomain: 'Portfolio · {{domain}}', manage: 'Manage', admin: 'Admin', + editProfile: 'Edit profile', 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 e1d5a76..13eab00 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -2597,6 +2597,7 @@ labUi: { portfolioDomain: 'Portafolio · {{domain}}', manage: 'Gestionar', admin: 'Admin', + editProfile: 'Editar perfil', 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/main.tsx b/src/main.tsx index b7c8c2c..c1f5324 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -16,10 +16,11 @@ if (typeof window !== 'undefined') { } } -// Clean share links: a bare /verify/ or /card/ hits the Netlify SPA -// fallback (index.html), but HashRouter needs the route in the hash — rewrite it. +// Clean share links: a bare /verify/, /card/ or /coach// +// hits the Netlify SPA fallback (index.html), but HashRouter needs the route in +// the hash — rewrite it. if (typeof window !== 'undefined' && !window.location.hash) { - const m = window.location.pathname.match(/^\/(verify|card)\/(.+)$/); + const m = window.location.pathname.match(/^\/(verify|card|coach)\/(.+)$/); if (m) window.location.replace(`/#/${m[1]}/${m[2]}${window.location.search}`); } diff --git a/src/pages/CoachDetailPage.tsx b/src/pages/CoachDetailPage.tsx index b61641c..5b3a51a 100644 --- a/src/pages/CoachDetailPage.tsx +++ b/src/pages/CoachDetailPage.tsx @@ -1,4 +1,5 @@ import { dateLocaleCode } from '@/lib/dateFnsI18n'; +import { useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { PageError } from '@/components/ErrorState'; @@ -41,6 +42,7 @@ import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coach import { parseLabExtras } from '@/lib/labExtras'; import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields'; import { LabExtrasSection } from '@/components/LabExtrasSection'; +import { ShareProfileDialog } from '@/components/ShareProfileDialog'; import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl'; import { useTranslation } from '@/contexts/language'; import { OfficialBadge } from '@/components/OfficialBadge'; @@ -71,6 +73,8 @@ import { Tent, Sparkles, ShieldCheck, + Share2, + Pencil, } from 'lucide-react'; import { nip19 } from 'nostr-tools'; @@ -110,6 +114,7 @@ export default function CoachDetailPage() { const author = useAuthor(profile?.pubkey); const { user } = useCurrentUser(); const navigate = useNavigate(); + const [shareOpen, setShareOpen] = useState(false); // Gigs — only relevant for musicians. The hook is disabled for other types. const { data: gigs = [] } = useGigs( @@ -360,6 +365,14 @@ export default function CoachDetailPage() { featuredUntil={profile.featuredUntil} entityCoord={`${profile.event.kind}:${profile.pubkey}:${profile.identifier}`} /> +

@@ -878,6 +891,13 @@ export default function CoachDetailPage() { ? t('labUi.detail.manageRemove', { noun: typeNoun }) : t('labUi.detail.adminTakeDown', { noun: typeNoun })}

+ {user.pubkey === profile.pubkey && ( + + + + )} + + ); }