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="/crew/:pubkey/:identifier" element={<CrewDetailPage />} />
|
||||
<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="/pyramid" element={<PyramidPage />} />
|
||||
<Route path="/marketplace" element={<MarketplaceHubPage />} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -47,7 +48,7 @@ export function CollectiveJoinRequestCard({
|
|||
const { toast } = useToast();
|
||||
|
||||
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.
|
||||
const { data: profile, isLoading: profileLoading } = useLabProfile(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { isValidNoffer, type EntityType } from '@/lib/types';
|
||||
import type { LabOption } from '@/lib/labExtras';
|
||||
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -310,7 +311,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}${existingProfile ? '' : '?published=1'}`);
|
||||
navigate(`${labProfilePathFromKind(kind, 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,4 +1,5 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
||||
import { Users, GraduationCap, ChevronRight, Link2, BadgeCheck } from 'lucide-react';
|
||||
|
|
@ -96,7 +97,7 @@ function LabRow({ coord, selfCoord }: { coord: string; selfCoord?: string }) {
|
|||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<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 { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { verifyEvent } from 'nostr-tools';
|
||||
|
||||
|
|
@ -621,7 +622,7 @@ export function useCVMClient() {
|
|||
experience: getTag(ev, 'experience'),
|
||||
lightning_address: getTag(ev, 'lud16') ?? getTag(ev, 'contact'),
|
||||
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>
|
||||
// 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>, /coach/<pubkey>/<d>
|
||||
// or /lab/<craft>/<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|coach)\/(.+)$/);
|
||||
const labProfile = window.location.pathname.match(/^\/lab\/[^/]+\/[^/]+\/[^/]+$/);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import {
|
||||
|
|
@ -644,7 +645,7 @@ function LabQueue() {
|
|||
image: p.image,
|
||||
imageCount: p.image ? 1 : 0,
|
||||
hasLocation: !!p.geohash,
|
||||
href: `/coach/${p.pubkey}/${p.identifier}`,
|
||||
href: labProfilePath(p.profileType, p.pubkey, p.identifier),
|
||||
}));
|
||||
return (
|
||||
<QueueList
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
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') });
|
||||
navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`);
|
||||
navigate(`${labProfilePath('builder', 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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { cn } from '@/lib/utils';
|
|||
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Check, Eye } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
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') });
|
||||
navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`);
|
||||
navigate(`${labProfilePath('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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
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 });
|
||||
|
||||
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) {
|
||||
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 { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
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') });
|
||||
navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`);
|
||||
navigate(`${labProfilePath('filmer', 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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
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') });
|
||||
navigate(`/coach/${user?.pubkey}/${identifier}${existingProfile ? '' : '?published=1'}`);
|
||||
navigate(`${labProfilePath('photographer', 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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coachOptionKey, coachRateCurrencyKey, coachRateUnitKey, type CoachOption } from '@/lib/coachOptions';
|
||||
import { parseLabExtras } from '@/lib/labExtras';
|
||||
import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { LabExtrasSection } from '@/components/LabExtrasSection';
|
||||
import { ShareProfileDialog } from '@/components/ShareProfileDialog';
|
||||
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
||||
|
|
@ -991,7 +992,7 @@ export default function CoachDetailPage() {
|
|||
onOpenChange={setShareOpen}
|
||||
npub={nip19.npubEncode(profile.pubkey)}
|
||||
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 { labProfilePath } from '@/lib/labRoutes';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
|
@ -416,7 +417,7 @@ export default function EventDetailPage() {
|
|||
)}
|
||||
{musicAct && (
|
||||
<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"
|
||||
>
|
||||
<Music className="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
||||
|
|
@ -427,7 +428,7 @@ function LabProfileCard({ profile }: { profile: LabProfile }) {
|
|||
const accentColor = isCoach ? '#ff0040' : '#ff6600';
|
||||
|
||||
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={{
|
||||
borderColor: `${accentColor}40`,
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
|
|||
import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter';
|
||||
import { COACH_FACETS, coachOptionKey, emptyCoachFilters, type CoachFacetKey } from '@/lib/coachOptions';
|
||||
import { parseLabExtras, type LabExtras } from '@/lib/labExtras';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { type SortOption } from '@/components/SortDropdown';
|
||||
import { useLabProfiles, type LabProfile } from '@/hooks/useLabProfiles';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
|
|
@ -1081,7 +1082,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
|||
const getSkillLabel = (skill: string) => getSkillLabelForType(skill, serviceType, t);
|
||||
|
||||
return (
|
||||
<Link to={`/coach/${profile.pubkey}/${profile.identifier}`}>
|
||||
<Link to={labProfilePath(profile.profileType, profile.pubkey, profile.identifier)}>
|
||||
<div className={cn(
|
||||
'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',
|
||||
|
|
@ -1642,7 +1643,7 @@ export default function LabPage() {
|
|||
{ownProfiles.map((p) => (
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<Pencil className="h-3 w-3 text-[#ccff00]" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
|
|
@ -889,7 +890,7 @@ export default function SpotDetailPage() {
|
|||
{spotServices.coaches.map((coach) => (
|
||||
<Link
|
||||
key={coach.id}
|
||||
to={`/coach/${coach.pubkey}/${coach.identifier}`}
|
||||
to={labProfilePath('coach', coach.pubkey, coach.identifier)}
|
||||
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">
|
||||
|
|
@ -932,7 +933,7 @@ export default function SpotDetailPage() {
|
|||
{spotServices.filmers.map((filmer) => (
|
||||
<Link
|
||||
key={filmer.id}
|
||||
to={`/coach/${filmer.pubkey}/${filmer.identifier}`}
|
||||
to={labProfilePath('filmer', filmer.pubkey, filmer.identifier)}
|
||||
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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue