From 19eaf1a4e301be0e55e5ab3430348dc40133d469 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 14 Nov 2025 08:02:21 -0600 Subject: [PATCH] feat: add lightbox to profile avatar and banner (#661) --- src/components/Profile/index.tsx | 11 ++-- src/components/ProfileBanner/index.tsx | 80 ++++++++++++++++++++++++++ src/components/UserAvatar/index.tsx | 75 +++++++++++++++++++++++- 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 4cc499d..3aa0543 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -3,12 +3,13 @@ import FollowButton from '@/components/FollowButton' import Nip05 from '@/components/Nip05' import NpubQrCode from '@/components/NpubQrCode' import ProfileAbout from '@/components/ProfileAbout' -import ProfileBanner from '@/components/ProfileBanner' +import { BannerWithLightbox } from '@/components/ProfileBanner' import ProfileOptions from '@/components/ProfileOptions' import ProfileZapButton from '@/components/ProfileZapButton' import PubkeyCopy from '@/components/PubkeyCopy' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' +import { AvatarWithLightbox } from '@/components/UserAvatar' import { useFetchFollowings, useFetchProfile } from '@/hooks' import { toMuteList, toProfileEditor } from '@/lib/link' import { SecondaryPageLink, useSecondaryPage } from '@/PageManager' @@ -20,7 +21,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import NotFound from '../NotFound' import SearchInput from '../SearchInput' -import { SimpleUserAvatar } from '../UserAvatar' import FollowedBy from './FollowedBy' import Followings from './Followings' import ProfileFeed from './ProfileFeed' @@ -114,10 +114,11 @@ export default function Profile({ id }: { id?: string }) { <>
- - +
diff --git a/src/components/ProfileBanner/index.tsx b/src/components/ProfileBanner/index.tsx index dbf025d..04d4080 100644 --- a/src/components/ProfileBanner/index.tsx +++ b/src/components/ProfileBanner/index.tsx @@ -1,6 +1,11 @@ import { generateImageByPubkey } from '@/lib/pubkey' +import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' +import modalManager from '@/services/modal-manager.service' import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' +import Lightbox from 'yet-another-react-lightbox' +import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Image from '../Image' export default function ProfileBanner({ @@ -32,3 +37,78 @@ export default function ProfileBanner({ /> ) } + +export function BannerWithLightbox({ + pubkey, + banner, + className +}: { + pubkey: string + banner?: string + className?: string +}) { + const id = useMemo(() => `profile-banner-lightbox-${randomString()}`, []) + const defaultBanner = useMemo(() => generateImageByPubkey(pubkey), [pubkey]) + const [bannerUrl, setBannerUrl] = useState(banner ?? defaultBanner) + const [index, setIndex] = useState(-1) + + useEffect(() => { + if (banner) { + setBannerUrl(banner) + } else { + setBannerUrl(defaultBanner) + } + }, [defaultBanner, banner]) + + useEffect(() => { + if (index >= 0) { + modalManager.register(id, () => { + setIndex(-1) + }) + } else { + modalManager.unregister(id) + } + }, [index, id]) + + const handleBannerClick = (event: React.MouseEvent) => { + event.stopPropagation() + event.preventDefault() + setIndex(0) + } + + return ( + <> + {`${pubkey} + {index >= 0 && + createPortal( +
e.stopPropagation()}> + = 0} + close={() => setIndex(-1)} + controller={{ + closeOnBackdropClick: true, + closeOnPullUp: true, + closeOnPullDown: true + }} + styles={{ + toolbar: { paddingTop: '2.25rem' } + }} + /> +
, + document.body + )} + + ) +} diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 38fa393..4782091 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -3,9 +3,14 @@ import { Skeleton } from '@/components/ui/skeleton' import { useFetchProfile } from '@/hooks' import { toProfile } from '@/lib/link' import { generateImageByPubkey } from '@/lib/pubkey' +import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' import { SecondaryPageLink } from '@/PageManager' -import { useMemo } from 'react' +import modalManager from '@/services/modal-manager.service' +import { useEffect, useMemo, useState } from 'react' +import { createPortal } from 'react-dom' +import Lightbox from 'yet-another-react-lightbox' +import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Image from '../Image' import ProfileCard from '../ProfileCard' @@ -79,3 +84,71 @@ export function SimpleUserAvatar({ /> ) } + +export function AvatarWithLightbox({ + userId, + size = 'normal', + className +}: { + userId: string + size?: 'large' | 'big' | 'semiBig' | 'normal' | 'medium' | 'small' | 'xSmall' | 'tiny' + className?: string +}) { + const id = useMemo(() => `user-avatar-lightbox-${randomString()}`, []) + const { profile } = useFetchProfile(userId) + const defaultAvatar = useMemo( + () => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), + [profile] + ) + const [index, setIndex] = useState(-1) + + useEffect(() => { + if (index >= 0) { + modalManager.register(id, () => { + setIndex(-1) + }) + } else { + modalManager.unregister(id) + } + }, [index, id]) + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + setIndex(0) + } + + const imageUrl = profile?.avatar ?? defaultAvatar + + return ( + <> + + {index >= 0 && + createPortal( +
e.stopPropagation()}> + = 0} + close={() => setIndex(-1)} + controller={{ + closeOnBackdropClick: true, + closeOnPullUp: true, + closeOnPullDown: true + }} + styles={{ + toolbar: { paddingTop: '2.25rem' } + }} + /> +
, + document.body + )} + + ) +}