Add owner Edit + Share to Lab provider profiles
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/<craft> (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/<pubkey>/<identifier>. - ShareProfileDialog accepts an optional `path` to override the default /<npub> share URL. - main.tsx clean-link shim now also rewrites /coach/<pubkey>/<d> 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.
This commit is contained in:
parent
681eab9592
commit
9d1a6999ea
5 changed files with 39 additions and 5 deletions
|
|
@ -13,6 +13,9 @@ interface ShareProfileDialogProps {
|
|||
npub: string;
|
||||
/** Display name, used in the share sheet + QR caption. */
|
||||
displayName: string;
|
||||
/** Optional path (e.g. `/coach/<pubkey>/<d>`) appended to the canonical origin
|
||||
* instead of the default `/<npub>` — 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<string | null>(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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}}',
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ if (typeof window !== 'undefined') {
|
|||
}
|
||||
}
|
||||
|
||||
// Clean share links: a bare /verify/<naddr> or /card/<npub> hits the Netlify SPA
|
||||
// fallback (index.html), but HashRouter needs the route in the hash — rewrite it.
|
||||
// Clean share links: a bare /verify/<naddr>, /card/<npub> or /coach/<pubkey>/<d>
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShareOpen(true)}
|
||||
className={`ml-auto h-7 gap-1.5 px-2.5 ${accent.border} ${accent.text} hover:bg-white/5`}
|
||||
>
|
||||
<Share2 className="h-3.5 w-3.5" /> {t('common.share')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-gray-400 flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
|
|
@ -878,6 +891,13 @@ export default function CoachDetailPage() {
|
|||
? t('labUi.detail.manageRemove', { noun: typeNoun })
|
||||
: t('labUi.detail.adminTakeDown', { noun: typeNoun })}
|
||||
</p>
|
||||
{user.pubkey === profile.pubkey && (
|
||||
<Link to={`/become/${profile.profileType}`}>
|
||||
<Button variant="outline" className={`w-full justify-center gap-2 ${accent.border} ${accent.text} hover:bg-white/5`}>
|
||||
<Pencil className="h-4 w-4" /> {t('labUi.detail.editProfile')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<DeleteEntityDialog
|
||||
event={profile.event}
|
||||
entityLabel={t('labUi.detail.entityLabel', { noun: typeNoun })}
|
||||
|
|
@ -936,6 +956,14 @@ export default function CoachDetailPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ShareProfileDialog
|
||||
open={shareOpen}
|
||||
onOpenChange={setShareOpen}
|
||||
npub={nip19.npubEncode(profile.pubkey)}
|
||||
displayName={profile.title}
|
||||
path={`/coach/${profile.pubkey}/${profile.identifier}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue