From d8a73007de3edd398bf332266ff65d01be9bcea5 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Tue, 17 Feb 2026 03:48:07 -0600 Subject: [PATCH] Refine profile view: rounded media tiles, accent-colored flat streak, favicons on field URLs - Media grid items now have individual rounded-lg corners with gap spacing - Images use sizes hint for better quality rendering at small sizes - Streak indicator: flat style with filled flame icon using theme accent color, positioned between username and description, 24h window, kind 1 only - Profile field URLs now show Google favicon next to each link with graceful fallback Co-authored-by: shakespeare.diy --- src/components/ProfileRightSidebar.tsx | 51 ++++++++++++++++++++------ src/pages/ProfilePage.tsx | 44 +++++++++++----------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/components/ProfileRightSidebar.tsx b/src/components/ProfileRightSidebar.tsx index 9d647ac5..a9b4d38c 100644 --- a/src/components/ProfileRightSidebar.tsx +++ b/src/components/ProfileRightSidebar.tsx @@ -1,12 +1,11 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; -import { Copy, Check, QrCode, ExternalLink, Bitcoin } from 'lucide-react'; +import { Check, QrCode, ExternalLink, Bitcoin } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from '@/hooks/useToast'; -import type { NostrEvent } from '@nostrify/nostrify'; import QRCode from 'qrcode'; interface ProfileField { @@ -53,13 +52,23 @@ function useProfileMedia(pubkey: string) { } } - return urls.slice(0, 9); // Max 9 items for a 3x3 grid + return urls.slice(0, 9); }, enabled: !!pubkey, staleTime: 5 * 60 * 1000, }); } +/** Get favicon URL for a given website URL using Google's favicon service. */ +function getFaviconUrl(url: string): string { + try { + const hostname = new URL(url).hostname; + return `https://www.google.com/s2/favicons?domain=${hostname}&sz=32`; + } catch { + return ''; + } +} + /** Bitcoin QR code modal */ function BitcoinQRModal({ address }: { address: string }) { const [qrUrl, setQrUrl] = useState(''); @@ -130,6 +139,24 @@ function BitcoinQRModal({ address }: { address: string }) { ); } +/** Favicon image with graceful fallback. */ +function Favicon({ url }: { url: string }) { + const faviconSrc = getFaviconUrl(url); + const [failed, setFailed] = useState(false); + + if (!faviconSrc || failed) return null; + + return ( + setFailed(true)} + /> + ); +} + /** A single profile field row. Handles $BTC specially. */ function ProfileFieldRow({ field }: { field: ProfileField }) { const [copied, setCopied] = useState(false); @@ -186,7 +213,7 @@ function ProfileFieldRow({ field }: { field: ProfileField }) { ); } - // Regular field: label + linked value + // Regular field: label + linked value with favicon const isUrl = field.value.startsWith('http://') || field.value.startsWith('https://'); return ( @@ -197,9 +224,10 @@ function ProfileFieldRow({ field }: { field: ProfileField }) { href={field.value} target="_blank" rel="noopener noreferrer" - className="text-sm text-primary hover:underline truncate block" + className="flex items-center gap-1.5 text-sm text-primary hover:underline truncate mt-0.5" > - {field.value} + + {field.value.replace(/^https?:\/\//, '')} ) : (

{field.value}

@@ -217,13 +245,13 @@ export function ProfileRightSidebar({ pubkey, fields }: ProfileRightSidebarProps

Media

{mediaLoading ? ( -
+
{Array.from({ length: 6 }).map((_, i) => ( - + ))}
) : media && media.length > 0 ? ( -
+
{media.map((url, i) => { const isVideo = /\.(mp4|webm|mov)(\?.*)?$/i.test(url); return ( @@ -232,7 +260,7 @@ export function ProfileRightSidebar({ pubkey, fields }: ProfileRightSidebarProps href={url} target="_blank" rel="noopener noreferrer" - className="aspect-square overflow-hidden hover:opacity-80 transition-opacity" + className="aspect-square rounded-lg overflow-hidden hover:opacity-80 transition-opacity" > {isVideo ? (
-
-
-

{displayName}

- {metadata?.nip05 && ( -

@{metadata.nip05}

- )} -
+

{displayName}

+ {metadata?.nip05 && ( +

@{metadata.nip05}

+ )} - {/* Streak badge */} - {streak > 1 && ( -
STREAK_DISPLAY_LIMIT ? `${STREAK_DISPLAY_LIMIT}+` : streak} posts within ${STREAK_WINDOW_HOURS}h windows`}> - - - {streak > STREAK_DISPLAY_LIMIT ? `${STREAK_DISPLAY_LIMIT}+` : streak} - -
- )} -
+ {/* Streak indicator */} + {streak > 1 && ( +
STREAK_DISPLAY_LIMIT ? `${STREAK_DISPLAY_LIMIT}+` : streak} posts within ${STREAK_WINDOW_HOURS}h windows`} + > + + + {streak > STREAK_DISPLAY_LIMIT ? `${STREAK_DISPLAY_LIMIT}+` : streak} + +
+ )} {metadata?.about && (

{metadata.about}