Skater profile: browse real map spots for home spot + always use kind-0 profile pic
Home spot: - Add "Browse spots" button opening a searchable dialog of real mapped spots (useSkateSpots) - Picking a spot stores its title + an `a` (30818:pubkey:d) coordinate; shows a linked-spot chip with unlink - Profile page renders a linked home spot as a link to /spot/:pubkey/:identifier - Parse homeSpotRef from the `a` tag; document in NIP.md Profile pic consistency: - Remove the skate-specific image override/upload — the skater card now always uses the kind-0 profile picture so both sections show the identical photo - "Profile Pic" section is read-only with a "Change profile photo" link to settings - Drop the `image` tag from published skater events; note the change in NIP.md Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
4c9839eb45
commit
bb6245e53c
4 changed files with 217 additions and 75 deletions
9
NIP.md
9
NIP.md
|
|
@ -1511,10 +1511,10 @@ exists purely for community discovery.
|
|||
["level_custom", "<free-text level, ≤40 chars, mutually exclusive with `level`>"],
|
||||
["t", "<style>"],
|
||||
["years", "<years skating>"],
|
||||
["home_spot", "<freeform home spot>"],
|
||||
["home_spot", "<freeform home spot, or the title of the linked spot>"],
|
||||
["a", "30818:<spot pubkey>:<spot d-tag> (optional — set when home_spot links a real mapped spot)"],
|
||||
["location", "<city / area>"],
|
||||
["g", "<geohash, coarse>"],
|
||||
["image", "<action shot url>"],
|
||||
["instagram", "<@handle>"],
|
||||
["website", "<url>"]
|
||||
]
|
||||
|
|
@ -1530,6 +1530,11 @@ Level is either one of the four presets via the `level` tag, **or** a
|
|||
free-text label via `level_custom` (max 40 chars) — clients should treat these
|
||||
as mutually exclusive and prefer `level` when both are present.
|
||||
|
||||
The skater's avatar always comes from their kind-0 profile picture — the
|
||||
skater profile does not carry its own image, so the two never diverge. When
|
||||
`home_spot` refers to a real mapped spot, an `a` tag holds the
|
||||
`30818:pubkey:d` coordinate so clients can link straight to it.
|
||||
|
||||
## The Lab — Service Profiles
|
||||
|
||||
The Lab connects skaters with service providers. Each provider type is an
|
||||
|
|
|
|||
|
|
@ -341,7 +341,8 @@ export interface Skater {
|
|||
level?: SkaterLevel;
|
||||
customLevel?: string; // free-text level when none of the presets fit
|
||||
yearsSkating?: string;
|
||||
homeSpot?: string; // freeform, or a spot ref
|
||||
homeSpot?: string; // freeform name, or the title of a linked spot
|
||||
homeSpotRef?: string; // `a` tag coordinate (`30818:pubkey:d`) when linked to a real mapped spot
|
||||
instagram?: string;
|
||||
website?: string;
|
||||
createdAt: number;
|
||||
|
|
@ -381,6 +382,7 @@ export function parseSkater(event: NostrEvent): Skater | null {
|
|||
customLevel: tag('level_custom')?.slice(0, 40),
|
||||
yearsSkating: tag('years'),
|
||||
homeSpot: tag('home_spot'),
|
||||
homeSpotRef: event.tags.find(([n, v]) => n === 'a' && v?.startsWith(`${SKATE_SPOT_KIND}:`))?.[1],
|
||||
instagram: tag('instagram'),
|
||||
website: tag('website'),
|
||||
createdAt: event.created_at,
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ import { Input } from '@/components/ui/input';
|
|||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { useUserSkaterProfile, SKATER_D } from '@/hooks/useSkaters';
|
||||
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
||||
import {
|
||||
SKATE_SKATER_KIND,
|
||||
SKATE_SPOT_KIND,
|
||||
SKATER_TAG,
|
||||
SKATER_STANCES,
|
||||
SKATER_STYLES,
|
||||
|
|
@ -25,10 +28,11 @@ import {
|
|||
type SkaterStance,
|
||||
type SkaterStyle,
|
||||
type SkaterLevel,
|
||||
type SkateSpot,
|
||||
} from '@/lib/types';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Upload, X, Loader2, ArrowLeft, MapPin, PersonStanding, Sparkles, UserRound, ShieldCheck, Zap, FileText, Globe } from 'lucide-react';
|
||||
import { X, Loader2, ArrowLeft, MapPin, PersonStanding, Sparkles, UserRound, ShieldCheck, Zap, FileText, Globe, Map as MapIcon, Search, Check } from 'lucide-react';
|
||||
|
||||
export default function BecomeSkaterPage() {
|
||||
useSeoMeta({
|
||||
|
|
@ -40,9 +44,9 @@ export default function BecomeSkaterPage() {
|
|||
const { user } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const { toast } = useToast();
|
||||
const { data: existing, isLoading: loadingExisting } = useUserSkaterProfile(user?.pubkey);
|
||||
const { data: allSpots, isLoading: loadingSpots } = useSkateSpots(500);
|
||||
|
||||
const metadata = author.data?.metadata;
|
||||
const displayName = metadata?.name ?? (user ? genUserName(user.pubkey) : 'Skater');
|
||||
|
|
@ -65,11 +69,12 @@ export default function BecomeSkaterPage() {
|
|||
const [levelMode, setLevelMode] = useState<'preset' | 'custom'>('preset');
|
||||
const [years, setYears] = useState('');
|
||||
const [homeSpot, setHomeSpot] = useState('');
|
||||
// When the home spot is a real mapped spot, we keep its `a` coordinate here.
|
||||
const [homeSpotRef, setHomeSpotRef] = useState<string | undefined>();
|
||||
const [spotPickerOpen, setSpotPickerOpen] = useState(false);
|
||||
const [spotSearch, setSpotSearch] = useState('');
|
||||
const [instagram, setInstagram] = useState('');
|
||||
const [website, setWebsite] = useState('');
|
||||
// `image` is an explicit skate-specific override; when null we fall back to
|
||||
// the kind-0 avatar (shown in the UI as "using your profile photo").
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [isGettingLocation, setIsGettingLocation] = useState(false);
|
||||
// Ensures we only auto-prefill from kind-0 once, and never clobber typing.
|
||||
const [prefilled, setPrefilled] = useState(false);
|
||||
|
|
@ -89,9 +94,9 @@ export default function BecomeSkaterPage() {
|
|||
}
|
||||
setYears(existing.yearsSkating || '');
|
||||
setHomeSpot(existing.homeSpot || '');
|
||||
setHomeSpotRef(existing.homeSpotRef);
|
||||
setInstagram(existing.instagram || '');
|
||||
setWebsite(existing.website || kind0Website);
|
||||
if (existing.image) setImage(existing.image);
|
||||
setPrefilled(true);
|
||||
} else if (author.data) {
|
||||
// New skater — seed the shared fields from their Nostr profile.
|
||||
|
|
@ -124,22 +129,25 @@ export default function BecomeSkaterPage() {
|
|||
);
|
||||
};
|
||||
|
||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
try {
|
||||
const tags = await uploadFile(files[0]);
|
||||
const url = tags[0]?.[1];
|
||||
if (url) {
|
||||
setImage(url);
|
||||
toast({ title: 'Photo uploaded' });
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Upload failed', description: error instanceof Error ? error.message : 'Failed', variant: 'destructive' });
|
||||
}
|
||||
e.target.value = '';
|
||||
// Pick a real mapped spot as the home spot — stores its title + `a` coordinate.
|
||||
const selectSpot = (spot: SkateSpot) => {
|
||||
setHomeSpot(spot.title);
|
||||
setHomeSpotRef(`${SKATE_SPOT_KIND}:${spot.pubkey}:${spot.identifier}`);
|
||||
setSpotPickerOpen(false);
|
||||
setSpotSearch('');
|
||||
};
|
||||
|
||||
const clearSpotLink = () => setHomeSpotRef(undefined);
|
||||
|
||||
const filteredSpots = (allSpots ?? []).filter((s) => {
|
||||
if (!spotSearch.trim()) return true;
|
||||
const q = spotSearch.toLowerCase();
|
||||
return (
|
||||
s.title.toLowerCase().includes(q) ||
|
||||
(s.location?.toLowerCase().includes(q) ?? false)
|
||||
);
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
|
@ -173,14 +181,17 @@ export default function BecomeSkaterPage() {
|
|||
for (const s of styles) tags.push(['t', s]);
|
||||
if (years.trim()) tags.push(['years', years.trim()]);
|
||||
if (homeSpot.trim()) tags.push(['home_spot', homeSpot.trim()]);
|
||||
// Link to the real mapped spot when one was picked from the map.
|
||||
if (homeSpotRef) tags.push(['a', homeSpotRef]);
|
||||
if (instagram.trim()) tags.push(['instagram', instagram.trim()]);
|
||||
// Store shared fields ONLY as overrides — if they match kind-0, leave them
|
||||
// out so kind-0 stays the single source of truth and nothing goes stale.
|
||||
// Photo is intentionally NOT stored here: the skater card always uses the
|
||||
// kind-0 profile picture so the two stay identical (single source of truth).
|
||||
const websiteTrim = website.trim();
|
||||
if (websiteTrim && websiteTrim !== kind0Website.trim()) {
|
||||
tags.push(['website', websiteTrim]);
|
||||
}
|
||||
if (image && image !== avatar) tags.push(['image', image]);
|
||||
|
||||
const bioTrim = bio.trim();
|
||||
const content = bioTrim === kind0About.trim() ? '' : bioTrim;
|
||||
|
|
@ -328,46 +339,33 @@ export default function BecomeSkaterPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Photo */}
|
||||
{/* Profile Pic — always your Nostr profile photo, no separate override */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-bold uppercase tracking-wide text-gray-400">Profile Pic</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
{image ? (
|
||||
<div className="relative w-28 h-28 rounded-2xl overflow-hidden bg-gray-800 flex-shrink-0">
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/80 hover:bg-red-500 transition-colors" aria-label="Remove photo">
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : avatar ? (
|
||||
// Falling back to the kind-0 avatar — no upload needed.
|
||||
<div className="relative w-28 h-28 rounded-2xl overflow-hidden bg-gray-800 flex-shrink-0 ring-2 ring-white/10">
|
||||
<img src={avatar} alt="Profile" className="w-full h-full object-cover opacity-90" />
|
||||
<span className="absolute bottom-0 inset-x-0 bg-black/70 text-[9px] font-bold uppercase tracking-wide text-center py-0.5 text-gray-300">
|
||||
Your profile pic
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<label className="w-28 h-28 rounded-2xl border-2 border-dashed border-white/15 flex flex-col items-center justify-center cursor-pointer hover:border-[#ff0040]/50 hover:bg-white/5 transition-colors flex-shrink-0">
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} disabled={isUploading} />
|
||||
{isUploading ? <Loader2 className="h-6 w-6 animate-spin text-gray-500" /> : <><Upload className="h-6 w-6 mb-1 text-gray-500" /><span className="text-[10px] text-gray-500 uppercase font-bold">Upload</span></>}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative w-28 h-28 rounded-2xl overflow-hidden bg-gray-800 flex-shrink-0 ring-2 ring-white/10">
|
||||
{avatar ? (
|
||||
<img src={avatar} alt="Profile" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-[#ff0040]/15 text-[#ff0040] text-2xl font-black">
|
||||
{displayName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="absolute bottom-0 inset-x-0 bg-black/70 text-[9px] font-bold uppercase tracking-wide text-center py-0.5 text-gray-300">
|
||||
Your profile pic
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
{image
|
||||
? 'A skate-specific shot for your card.'
|
||||
: avatar
|
||||
? 'Using your profile photo. Upload an action shot to override it here.'
|
||||
: 'Add an action shot for your skater card (optional).'}
|
||||
Your skater card uses your Nostr profile photo — same picture everywhere.
|
||||
</p>
|
||||
{!image && avatar && (
|
||||
<label className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-[#ff0040] cursor-pointer hover:underline">
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} disabled={isUploading} />
|
||||
{isUploading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Upload className="h-3.5 w-3.5" />}
|
||||
Upload action shot
|
||||
</label>
|
||||
)}
|
||||
<Link
|
||||
to="/settings?section=profile"
|
||||
className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-[#ff0040] hover:underline"
|
||||
>
|
||||
<UserRound className="h-3.5 w-3.5" />
|
||||
Change profile photo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -489,15 +487,57 @@ export default function BecomeSkaterPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="years" className="text-sm font-bold uppercase tracking-wide text-gray-400">Years skating</Label>
|
||||
<Input id="years" placeholder="e.g. 8" value={years} onChange={(e) => setYears(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="years" className="text-sm font-bold uppercase tracking-wide text-gray-400">Years skating</Label>
|
||||
<Input id="years" placeholder="e.g. 8" value={years} onChange={(e) => setYears(e.target.value)} className="max-w-[160px]" />
|
||||
</div>
|
||||
|
||||
{/* Home spot — free text OR link a real mapped spot */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="home" className="text-sm font-bold uppercase tracking-wide text-gray-400">Home spot</Label>
|
||||
<Input id="home" placeholder="Local park / plaza" value={homeSpot} onChange={(e) => setHomeSpot(e.target.value)} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSpotPickerOpen(true)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-[#ccff00] hover:underline"
|
||||
>
|
||||
<MapIcon className="h-3.5 w-3.5" />
|
||||
Browse spots
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{homeSpotRef ? (
|
||||
// A real spot from the map is linked.
|
||||
<div className="flex items-center gap-2 rounded-xl border border-[#ccff00]/40 bg-[#ccff00]/5 px-3 py-2.5">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-[#ccff00]/15 flex items-center justify-center">
|
||||
<MapPin className="h-4 w-4 text-[#ccff00]" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-white truncate">{homeSpot || 'Mapped spot'}</p>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide text-[#ccff00] flex items-center gap-1">
|
||||
<Check className="h-3 w-3" /> Linked to a spot on the map
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSpotLink}
|
||||
className="p-1.5 rounded-full hover:bg-white/10 transition-colors flex-shrink-0"
|
||||
aria-label="Unlink spot"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
id="home"
|
||||
placeholder="Local park / plaza — or tap “Browse spots”"
|
||||
value={homeSpot}
|
||||
onChange={(e) => setHomeSpot(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Type it freeform, or link a real spot from the map so people can go there.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -534,7 +574,7 @@ export default function BecomeSkaterPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button type="submit" className="w-full h-12 bg-[#ff0040] hover:bg-[#ff0040]/90 uppercase font-black text-base" disabled={isPublishing || isUploading}>
|
||||
<Button type="submit" className="w-full h-12 bg-[#ff0040] hover:bg-[#ff0040]/90 uppercase font-black text-base" disabled={isPublishing}>
|
||||
{isPublishing ? (
|
||||
<><Loader2 className="h-5 w-5 mr-2 animate-spin" />Publishing…</>
|
||||
) : (
|
||||
|
|
@ -544,6 +584,88 @@ export default function BecomeSkaterPage() {
|
|||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Spot picker: browse real mapped spots to set as home spot ── */}
|
||||
<Dialog open={spotPickerOpen} onOpenChange={setSpotPickerOpen}>
|
||||
<DialogContent className="max-w-lg bg-black border-white/10 p-0 gap-0 overflow-hidden">
|
||||
<DialogHeader className="p-5 pb-3 border-b border-white/10">
|
||||
<DialogTitle className="flex items-center gap-2 uppercase font-black">
|
||||
<MapIcon className="h-5 w-5 text-[#ccff00]" />
|
||||
Pick your home spot
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-4 pb-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Search spots by name or city…"
|
||||
value={spotSearch}
|
||||
onChange={(e) => setSpotSearch(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="max-h-[50vh] px-4 pb-4">
|
||||
{loadingSpots ? (
|
||||
<div className="flex items-center justify-center py-12 text-gray-500">
|
||||
<Loader2 className="h-5 w-5 animate-spin mr-2" /> Loading spots…
|
||||
</div>
|
||||
) : filteredSpots.length === 0 ? (
|
||||
<div className="text-center py-12 px-4">
|
||||
<MapPin className="h-8 w-8 mx-auto text-gray-600 mb-3" />
|
||||
<p className="text-sm text-gray-400">
|
||||
{spotSearch.trim() ? 'No spots match your search.' : 'No spots found yet.'}
|
||||
</p>
|
||||
<Link to="/map" className="text-xs font-bold uppercase tracking-wide text-[#ccff00] hover:underline mt-2 inline-block">
|
||||
Open the map
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{filteredSpots.slice(0, 100).map((spot) => {
|
||||
const coord = `${SKATE_SPOT_KIND}:${spot.pubkey}:${spot.identifier}`;
|
||||
const selected = coord === homeSpotRef;
|
||||
return (
|
||||
<button
|
||||
key={spot.id}
|
||||
type="button"
|
||||
onClick={() => selectSpot(spot)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 p-2.5 rounded-xl border text-left transition-all',
|
||||
selected
|
||||
? 'border-[#ccff00] bg-[#ccff00]/10'
|
||||
: 'border-white/10 bg-white/5 hover:border-[#ccff00]/40 hover:bg-white/[0.07]',
|
||||
)}
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg overflow-hidden bg-gray-800">
|
||||
{spot.images[0] ? (
|
||||
<img src={spot.images[0]} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<MapPin className="h-5 w-5 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-white truncate">{spot.title}</p>
|
||||
{spot.location && (
|
||||
<p className="text-xs text-gray-400 truncate flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3 flex-shrink-0" /> {spot.location}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{selected && <Check className="h-4 w-4 text-[#ccff00] flex-shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useUserPublicStash } from '@/hooks/useStashShowcase';
|
|||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SKATER_STANCES, SKATER_STYLES, SKATER_LEVELS } from '@/lib/types';
|
||||
import NotFound from './NotFound';
|
||||
import {
|
||||
|
|
@ -290,15 +291,27 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{skater?.homeSpot && (
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Home className="h-4 w-4 text-[#ff0040] flex-shrink-0" />
|
||||
<div className="leading-tight min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold">Home spot</p>
|
||||
<p className="text-white font-bold truncate">{skater.homeSpot}</p>
|
||||
{skater?.homeSpot && (() => {
|
||||
// Link to the mapped spot when the skater linked one.
|
||||
const parts = skater.homeSpotRef?.split(':');
|
||||
const spotHref = parts && parts.length >= 3
|
||||
? `/spot/${parts[1]}/${parts.slice(2).join(':')}`
|
||||
: undefined;
|
||||
const inner = (
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Home className="h-4 w-4 text-[#ff0040] flex-shrink-0" />
|
||||
<div className="leading-tight min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold">Home spot</p>
|
||||
<p className={cn('text-white font-bold truncate', spotHref && 'group-hover/home:text-[#ccff00] group-hover/home:underline')}>
|
||||
{skater.homeSpot}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
return spotHref ? (
|
||||
<Link to={spotHref} className="group/home min-w-0">{inner}</Link>
|
||||
) : inner;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Styles */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue