- First-visit language auto-detect (Spanish devices land in es; stored choice wins). - Sasquatch AI replies in the app language (skate trick names kept English). - i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t() key missing from en/es or any en/es key drift. - ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace, trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters. - Policy: only skate TRICK names stay English; user-generated content untouched.
88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { ExternalLink, ShieldCheck } from 'lucide-react';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { ProfileBadges } from '@/components/ProfileBadges';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { isSkateSpotsNip05, profileBadges } from '@/lib/badges';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* A live preview of how the signed-in user appears to everyone else — banner,
|
|
* avatar, handle, bio and badges — shown at the top of the Edit Profile screen
|
|
* so "how do I look?" has an answer without leaving the page.
|
|
*
|
|
* Reads the same kind-0 metadata the form edits, so it reflects the last SAVED
|
|
* profile (the form's unsaved edits aren't shown; the sticky Save button commits).
|
|
*/
|
|
export function ProfilePreviewCard({ className }: { className?: string }) {
|
|
const { user, metadata, isLoadingProfile } = useCurrentUser();
|
|
const { t } = useTranslation();
|
|
const [bannerOk, setBannerOk] = useState(true);
|
|
|
|
if (!user) return null;
|
|
|
|
const name = metadata?.display_name || metadata?.name || '';
|
|
const initials = (name || user.pubkey.slice(0, 2)).slice(0, 2).toUpperCase();
|
|
const nip05 = metadata?.nip05;
|
|
const about = metadata?.about?.trim();
|
|
const badges = profileBadges(metadata);
|
|
|
|
return (
|
|
<div className={cn('overflow-hidden rounded-2xl border border-white/10 bg-white/2', className)}>
|
|
{/* Banner (falls back to the brand gradient) */}
|
|
<div className="relative h-28 w-full bg-gradient-to-r from-[#ff0040]/25 via-[#ff6600]/15 to-[#ccff00]/20">
|
|
{metadata?.banner && bannerOk && (
|
|
<img
|
|
src={metadata.banner}
|
|
alt=""
|
|
className="h-full w-full object-cover"
|
|
onError={() => setBannerOk(false)}
|
|
/>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
|
|
<Link
|
|
to={`/${nip19.npubEncode(user.pubkey)}`}
|
|
className="absolute right-2 top-2 inline-flex items-center gap-1.5 rounded-full bg-black/55 px-3 py-1.5 text-xs font-bold text-white backdrop-blur transition-colors hover:bg-black/75"
|
|
>
|
|
{t('labUi.profilePreview.viewPublic')}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Identity */}
|
|
<div className="px-4 pb-4">
|
|
<div className="flex items-end gap-3">
|
|
<Avatar className="-mt-10 h-20 w-20 border-4 border-background shadow-lg">
|
|
{metadata?.picture && <AvatarImage src={metadata.picture} alt={name} />}
|
|
<AvatarFallback className="text-lg font-black">{initials}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="min-w-0 flex-1 pb-1">
|
|
<p className="truncate text-lg font-black leading-tight text-white">
|
|
{name || (isLoadingProfile ? t('common.loading') : t('labUi.profilePreview.unnamed'))}
|
|
</p>
|
|
{nip05 ? (
|
|
<span
|
|
className={cn(
|
|
'mt-0.5 inline-flex items-center gap-1 text-xs font-bold',
|
|
isSkateSpotsNip05(nip05) ? 'text-[#ccff00]' : 'text-blue-400',
|
|
)}
|
|
>
|
|
<ShieldCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
|
<span className="truncate">{nip05}</span>
|
|
</span>
|
|
) : (
|
|
<span className="mt-0.5 block text-xs text-muted-foreground">{t('labUi.profilePreview.noHandle')}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{about && <p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted-foreground">{about}</p>}
|
|
|
|
{badges.length > 0 && <ProfileBadges badges={badges} size="sm" className="mt-3" />}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|