Give Lab profiles a per-craft URL: /lab/<craft>/<pubkey>/<id>
Every provider lived at /coach/<pubkey>/<id> regardless of craft (a barber got a "coach" URL). New src/lib/labRoutes.ts maps craft kind → segment and builds the canonical path; the router adds /lab/:craft/:pubkey/:identifier (the craft segment is cosmetic — the page still resolves by pubkey + identifier) and keeps /coach/... working for old links. All generated links updated: offer-form redirects, Lab cards + "Your Lab profiles", cross-links, spot/event/admin links, the CVM client URL, the share path, and the SPA clean-link shim.
This commit is contained in:
parent
81b5fd0fab
commit
c794d5fd18
18 changed files with 78 additions and 20 deletions
|
|
@ -127,6 +127,8 @@ export function AppRouter() {
|
||||||
<Route path="/edit-crew/:pubkey/:identifier" element={<EditCrewPage />} />
|
<Route path="/edit-crew/:pubkey/:identifier" element={<EditCrewPage />} />
|
||||||
<Route path="/crew/:pubkey/:identifier" element={<CrewDetailPage />} />
|
<Route path="/crew/:pubkey/:identifier" element={<CrewDetailPage />} />
|
||||||
<Route path="/coach/:pubkey/:identifier" element={<CoachDetailPage />} />
|
<Route path="/coach/:pubkey/:identifier" element={<CoachDetailPage />} />
|
||||||
|
{/* Canonical per-craft provider URL (the craft segment is cosmetic). */}
|
||||||
|
<Route path="/lab/:craft/:pubkey/:identifier" element={<CoachDetailPage />} />
|
||||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||||
<Route path="/pyramid" element={<PyramidPage />} />
|
<Route path="/pyramid" element={<PyramidPage />} />
|
||||||
<Route path="/marketplace" element={<MarketplaceHubPage />} />
|
<Route path="/marketplace" element={<MarketplaceHubPage />} />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -47,7 +48,7 @@ export function CollectiveJoinRequestCard({
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const coord = parseAddrCoord(request.collectiveCoord);
|
const coord = parseAddrCoord(request.collectiveCoord);
|
||||||
const collectiveHref = coord ? `/coach/${coord.pubkey}/${coord.identifier}` : '/lab';
|
const collectiveHref = coord ? labProfilePathFromKind(coord.kind, coord.pubkey, coord.identifier) : '/lab';
|
||||||
|
|
||||||
// Resolve the collective so we can add the member in one click.
|
// Resolve the collective so we can add the member in one click.
|
||||||
const { data: profile, isLoading: profileLoading } = useLabProfile(
|
const { data: profile, isLoading: profileLoading } = useLabProfile(
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
import { isValidNoffer, type EntityType } from '@/lib/types';
|
import { isValidNoffer, type EntityType } from '@/lib/types';
|
||||||
import type { LabOption } from '@/lib/labExtras';
|
import type { LabOption } from '@/lib/labExtras';
|
||||||
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
|
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
|
||||||
import { useTranslation } from '@/contexts/language';
|
import { useTranslation } from '@/contexts/language';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -310,7 +311,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePathFromKind(kind, 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,4 +1,5 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
||||||
import { Users, GraduationCap, ChevronRight, Link2, BadgeCheck } from 'lucide-react';
|
import { Users, GraduationCap, ChevronRight, Link2, BadgeCheck } from 'lucide-react';
|
||||||
|
|
@ -96,7 +97,7 @@ function LabRow({ coord, selfCoord }: { coord: string; selfCoord?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/coach/${parsed.pubkey}/${parsed.identifier}`}
|
to={labProfilePathFromKind(parsed.kind, parsed.pubkey, parsed.identifier)}
|
||||||
className="flex items-center gap-3 rounded-lg border border-gray-700 bg-black/40 p-2.5 hover:bg-black/60 hover:border-teal-500/40 transition-colors"
|
className="flex items-center gap-3 rounded-lg border border-gray-700 bg-black/40 p-2.5 hover:bg-black/60 hover:border-teal-500/40 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-white/5">
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-white/5">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
import { useNostr } from '@nostrify/react';
|
import { useNostr } from '@nostrify/react';
|
||||||
import { verifyEvent } from 'nostr-tools';
|
import { verifyEvent } from 'nostr-tools';
|
||||||
|
|
||||||
|
|
@ -621,7 +622,7 @@ export function useCVMClient() {
|
||||||
experience: getTag(ev, 'experience'),
|
experience: getTag(ev, 'experience'),
|
||||||
lightning_address: getTag(ev, 'lud16') ?? getTag(ev, 'contact'),
|
lightning_address: getTag(ev, 'lud16') ?? getTag(ev, 'contact'),
|
||||||
naddr: buildNaddr(ev),
|
naddr: buildNaddr(ev),
|
||||||
url: `https://skate-spots.org/coach/${ev.pubkey}/${getTag(ev, 'd') ?? ''}`,
|
url: `https://skate-spots.org${labProfilePathFromKind(ev.kind, ev.pubkey, getTag(ev, 'd') ?? '')}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
39
src/lib/labRoutes.ts
Normal file
39
src/lib/labRoutes.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import {
|
||||||
|
SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND,
|
||||||
|
SKATE_BUILDER_KIND, SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND,
|
||||||
|
SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_KIND,
|
||||||
|
SKATE_BARBER_KIND,
|
||||||
|
} from '@/lib/types';
|
||||||
|
|
||||||
|
/** Lab craft kind → canonical URL segment (matches `becomeRoles` slugs). */
|
||||||
|
export const LAB_TYPE_BY_KIND: Record<number, string> = {
|
||||||
|
[SKATE_COACH_KIND]: 'coach',
|
||||||
|
[SKATE_FILMER_KIND]: 'filmer',
|
||||||
|
[SKATE_EDITOR_KIND]: 'editor',
|
||||||
|
[SKATE_PHOTOGRAPHER_KIND]: 'photographer',
|
||||||
|
[SKATE_BUILDER_KIND]: 'builder',
|
||||||
|
[SKATE_ARTIST_KIND]: 'artist',
|
||||||
|
[SKATE_GRAFFITI_KIND]: 'graffiti',
|
||||||
|
[SKATE_TATTOO_KIND]: 'tattoo',
|
||||||
|
[SKATE_TRAINER_KIND]: 'trainer',
|
||||||
|
[SKATE_MUSICIAN_KIND]: 'musician',
|
||||||
|
[SKATE_JUDGE_KIND]: 'judge',
|
||||||
|
[SKATE_ORGANIZER_KIND]: 'organizer',
|
||||||
|
[SKATE_BARBER_KIND]: 'barber',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical path for a Lab provider profile: `/lab/<craft>/<pubkey>/<identifier>`.
|
||||||
|
*
|
||||||
|
* The craft segment is cosmetic (the detail page resolves by pubkey + identifier
|
||||||
|
* across all kinds) but gives each craft a meaningful, shareable URL instead of
|
||||||
|
* every provider living under `/coach/…`.
|
||||||
|
*/
|
||||||
|
export function labProfilePath(profileType: string, pubkey: string, identifier: string): string {
|
||||||
|
return `/lab/${profileType}/${pubkey}/${identifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same as {@link labProfilePath} but from a kind number (e.g. a coord). */
|
||||||
|
export function labProfilePathFromKind(kind: number, pubkey: string, identifier: string): string {
|
||||||
|
return labProfilePath(LAB_TYPE_BY_KIND[kind] ?? 'coach', pubkey, identifier);
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,14 @@ if (typeof window !== 'undefined') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean share links: a bare /verify/<naddr>, /card/<npub> or /coach/<pubkey>/<d>
|
// Clean share links: a bare /verify/<naddr>, /card/<npub>, /coach/<pubkey>/<d>
|
||||||
// hits the Netlify SPA fallback (index.html), but HashRouter needs the route in
|
// or /lab/<craft>/<pubkey>/<d> hits the Netlify SPA fallback (index.html), but
|
||||||
// the hash — rewrite it.
|
// HashRouter needs the route in the hash — rewrite it.
|
||||||
if (typeof window !== 'undefined' && !window.location.hash) {
|
if (typeof window !== 'undefined' && !window.location.hash) {
|
||||||
const m = window.location.pathname.match(/^\/(verify|card|coach)\/(.+)$/);
|
const m = window.location.pathname.match(/^\/(verify|card|coach)\/(.+)$/);
|
||||||
|
const labProfile = window.location.pathname.match(/^\/lab\/[^/]+\/[^/]+\/[^/]+$/);
|
||||||
if (m) window.location.replace(`/#/${m[1]}/${m[2]}${window.location.search}`);
|
if (m) window.location.replace(`/#/${m[1]}/${m[2]}${window.location.search}`);
|
||||||
|
else if (labProfile) window.location.replace(`/#${window.location.pathname}${window.location.search}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outfit font for body text
|
// Outfit font for body text
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import {
|
import {
|
||||||
|
|
@ -644,7 +645,7 @@ function LabQueue() {
|
||||||
image: p.image,
|
image: p.image,
|
||||||
imageCount: p.image ? 1 : 0,
|
imageCount: p.image ? 1 : 0,
|
||||||
hasLocation: !!p.geohash,
|
hasLocation: !!p.geohash,
|
||||||
href: `/coach/${p.pubkey}/${p.identifier}`,
|
href: labProfilePath(p.profileType, p.pubkey, p.identifier),
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<QueueList
|
<QueueList
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { pickRateUnits } from '@/lib/labExtras';
|
import { pickRateUnits } from '@/lib/labExtras';
|
||||||
|
|
||||||
const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS);
|
const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS);
|
||||||
|
|
@ -326,7 +327,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePath('builder', 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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { cn } from '@/lib/utils';
|
||||||
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Check, Eye } from 'lucide-react';
|
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Check, Eye } from 'lucide-react';
|
||||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
|
|
||||||
|
|
@ -413,7 +414,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePath('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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { pickRateUnits } from '@/lib/labExtras';
|
import { pickRateUnits } from '@/lib/labExtras';
|
||||||
|
|
||||||
const SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS);
|
const SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS);
|
||||||
|
|
@ -251,7 +252,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePath('editor', 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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { pickRateUnits } from '@/lib/labExtras';
|
import { pickRateUnits } from '@/lib/labExtras';
|
||||||
|
|
||||||
const SERVICE_ENTRIES = Object.entries(FILMING_SERVICE_LABELS);
|
const SERVICE_ENTRIES = Object.entries(FILMING_SERVICE_LABELS);
|
||||||
|
|
@ -283,7 +284,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePath('filmer', 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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { pickRateUnits } from '@/lib/labExtras';
|
import { pickRateUnits } from '@/lib/labExtras';
|
||||||
|
|
||||||
const SERVICE_ENTRIES = Object.entries(PHOTO_SERVICE_LABELS);
|
const SERVICE_ENTRIES = Object.entries(PHOTO_SERVICE_LABELS);
|
||||||
|
|
@ -350,7 +351,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}${existingProfile ? '' : '?published=1'}`);
|
navigate(`${labProfilePath('photographer', 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' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import {
|
||||||
import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coachOptionKey, coachRateCurrencyKey, coachRateUnitKey, type CoachOption } from '@/lib/coachOptions';
|
import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coachOptionKey, coachRateCurrencyKey, coachRateUnitKey, type CoachOption } from '@/lib/coachOptions';
|
||||||
import { parseLabExtras } from '@/lib/labExtras';
|
import { parseLabExtras } from '@/lib/labExtras';
|
||||||
import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields';
|
import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { LabExtrasSection } from '@/components/LabExtrasSection';
|
import { LabExtrasSection } from '@/components/LabExtrasSection';
|
||||||
import { ShareProfileDialog } from '@/components/ShareProfileDialog';
|
import { ShareProfileDialog } from '@/components/ShareProfileDialog';
|
||||||
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
||||||
|
|
@ -991,7 +992,7 @@ export default function CoachDetailPage() {
|
||||||
onOpenChange={setShareOpen}
|
onOpenChange={setShareOpen}
|
||||||
npub={nip19.npubEncode(profile.pubkey)}
|
npub={nip19.npubEncode(profile.pubkey)}
|
||||||
displayName={profile.title}
|
displayName={profile.title}
|
||||||
path={`/coach/${profile.pubkey}/${profile.identifier}`}
|
path={labProfilePath(profile.profileType, profile.pubkey, profile.identifier)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
|
@ -416,7 +417,7 @@ export default function EventDetailPage() {
|
||||||
)}
|
)}
|
||||||
{musicAct && (
|
{musicAct && (
|
||||||
<Link
|
<Link
|
||||||
to={`/coach/${musicAct.pubkey}/${musicAct.identifier}`}
|
to={labProfilePath('musician', musicAct.pubkey, musicAct.identifier)}
|
||||||
className="inline-flex items-center gap-1.5 rounded-full border border-fuchsia-400/50 bg-fuchsia-500/20 px-3 py-1 text-sm font-bold text-fuchsia-200 backdrop-blur transition-colors hover:bg-fuchsia-500/30"
|
className="inline-flex items-center gap-1.5 rounded-full border border-fuchsia-400/50 bg-fuchsia-500/20 px-3 py-1 text-sm font-bold text-fuchsia-200 backdrop-blur transition-colors hover:bg-fuchsia-500/30"
|
||||||
>
|
>
|
||||||
<Music className="h-3.5 w-3.5" />
|
<Music className="h-3.5 w-3.5" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
||||||
|
|
@ -427,7 +428,7 @@ function LabProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
const accentColor = isCoach ? '#ff0040' : '#ff6600';
|
const accentColor = isCoach ? '#ff0040' : '#ff6600';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/coach/${profile.pubkey}/${profile.identifier}`}>
|
<Link to={labProfilePath(profile.profileType, profile.pubkey, profile.identifier)}>
|
||||||
<Card className="bg-black/90 border-2 hover:shadow-2xl transition-all h-full backdrop-blur" style={{
|
<Card className="bg-black/90 border-2 hover:shadow-2xl transition-all h-full backdrop-blur" style={{
|
||||||
borderColor: `${accentColor}40`,
|
borderColor: `${accentColor}40`,
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
|
||||||
import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter';
|
import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter';
|
||||||
import { COACH_FACETS, coachOptionKey, emptyCoachFilters, type CoachFacetKey } from '@/lib/coachOptions';
|
import { COACH_FACETS, coachOptionKey, emptyCoachFilters, type CoachFacetKey } from '@/lib/coachOptions';
|
||||||
import { parseLabExtras, type LabExtras } from '@/lib/labExtras';
|
import { parseLabExtras, type LabExtras } from '@/lib/labExtras';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { type SortOption } from '@/components/SortDropdown';
|
import { type SortOption } from '@/components/SortDropdown';
|
||||||
import { useLabProfiles, type LabProfile } from '@/hooks/useLabProfiles';
|
import { useLabProfiles, type LabProfile } from '@/hooks/useLabProfiles';
|
||||||
import { useAuthor } from '@/hooks/useAuthor';
|
import { useAuthor } from '@/hooks/useAuthor';
|
||||||
|
|
@ -1081,7 +1082,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
const getSkillLabel = (skill: string) => getSkillLabelForType(skill, serviceType, t);
|
const getSkillLabel = (skill: string) => getSkillLabelForType(skill, serviceType, t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/coach/${profile.pubkey}/${profile.identifier}`}>
|
<Link to={labProfilePath(profile.profileType, profile.pubkey, profile.identifier)}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'group relative flex gap-3 p-4 rounded-2xl bg-white/3 border border-white/8',
|
'group relative flex gap-3 p-4 rounded-2xl bg-white/3 border border-white/8',
|
||||||
'hover:bg-white/5 hover:border-white/15 hover:shadow-xl',
|
'hover:bg-white/5 hover:border-white/15 hover:shadow-xl',
|
||||||
|
|
@ -1642,7 +1643,7 @@ export default function LabPage() {
|
||||||
{ownProfiles.map((p) => (
|
{ownProfiles.map((p) => (
|
||||||
<Link
|
<Link
|
||||||
key={p.id}
|
key={p.id}
|
||||||
to={`/coach/${p.pubkey}/${p.identifier}`}
|
to={labProfilePath(p.profileType, 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"
|
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]" />
|
<Pencil className="h-3 w-3 text-[#ccff00]" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
||||||
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, 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 } from 'react-router-dom';
|
||||||
|
|
@ -889,7 +890,7 @@ export default function SpotDetailPage() {
|
||||||
{spotServices.coaches.map((coach) => (
|
{spotServices.coaches.map((coach) => (
|
||||||
<Link
|
<Link
|
||||||
key={coach.id}
|
key={coach.id}
|
||||||
to={`/coach/${coach.pubkey}/${coach.identifier}`}
|
to={labProfilePath('coach', coach.pubkey, coach.identifier)}
|
||||||
className="block"
|
className="block"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 p-4 rounded-lg bg-lime-500/5 border border-lime-500/20 hover:bg-lime-500/10 hover:border-lime-500/40 transition-colors">
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-lime-500/5 border border-lime-500/20 hover:bg-lime-500/10 hover:border-lime-500/40 transition-colors">
|
||||||
|
|
@ -932,7 +933,7 @@ export default function SpotDetailPage() {
|
||||||
{spotServices.filmers.map((filmer) => (
|
{spotServices.filmers.map((filmer) => (
|
||||||
<Link
|
<Link
|
||||||
key={filmer.id}
|
key={filmer.id}
|
||||||
to={`/coach/${filmer.pubkey}/${filmer.identifier}`}
|
to={labProfilePath('filmer', filmer.pubkey, filmer.identifier)}
|
||||||
className="block"
|
className="block"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 p-4 rounded-lg bg-purple-500/5 border border-purple-500/20 hover:bg-purple-500/10 hover:border-purple-500/40 transition-colors">
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-purple-500/5 border border-purple-500/20 hover:bg-purple-500/10 hover:border-purple-500/40 transition-colors">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue