refactor: 💨

This commit is contained in:
codytseng
2025-11-14 22:51:37 +08:00
parent f77c228a02
commit bcafbcc48c
7 changed files with 87 additions and 168 deletions

View File

@@ -13,13 +13,15 @@ import Image from '../Image'
export default function ImageWithLightbox({
image,
className,
classNames = {}
classNames = {},
errorPlaceholder
}: {
image: TImetaInfo
className?: string
classNames?: {
wrapper?: string
}
errorPlaceholder?: string
}) {
const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])
const { t } = useTranslation()
@@ -67,6 +69,7 @@ export default function ImageWithLightbox({
}}
image={image}
onClick={(e) => handlePhotoClick(e)}
errorPlaceholder={errorPlaceholder}
/>
{index >= 0 &&
createPortal(

View File

@@ -0,0 +1,32 @@
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
import { generateImageByPubkey } from '@/lib/pubkey'
import { useMemo } from 'react'
import ImageWithLightbox from '../ImageWithLightbox'
export default function AvatarWithLightbox({ userId }: { userId: string }) {
const { profile } = useFetchProfile(userId)
const defaultAvatar = useMemo(
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''),
[profile]
)
if (!profile) {
return (
<Skeleton className="shrink-0 w-24 h-24 rounded-full absolute left-3 bottom-0 translate-y-1/2 border-4 border-background" />
)
}
const { avatar, pubkey } = profile || {}
return (
<ImageWithLightbox
image={{ url: avatar ?? defaultAvatar, pubkey }}
errorPlaceholder={defaultAvatar}
className="object-cover object-center"
classNames={{
wrapper:
'shrink-0 rounded-full bg-background w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background'
}}
/>
)
}

View File

@@ -0,0 +1,33 @@
import { generateImageByPubkey } from '@/lib/pubkey'
import { useEffect, useMemo, useState } from 'react'
import ImageWithLightbox from '../ImageWithLightbox'
export default function BannerWithLightbox({
pubkey,
banner
}: {
pubkey: string
banner?: string
}) {
const defaultBanner = useMemo(() => generateImageByPubkey(pubkey), [pubkey])
const [bannerUrl, setBannerUrl] = useState(banner ?? defaultBanner)
useEffect(() => {
if (banner) {
setBannerUrl(banner)
} else {
setBannerUrl(defaultBanner)
}
}, [defaultBanner, banner])
return (
<ImageWithLightbox
image={{ url: bannerUrl, pubkey }}
className="rounded-none w-full aspect-[3/1]"
classNames={{
wrapper: 'rounded-none border-none'
}}
errorPlaceholder={defaultBanner}
/>
)
}

View File

@@ -3,13 +3,11 @@ import FollowButton from '@/components/FollowButton'
import Nip05 from '@/components/Nip05'
import NpubQrCode from '@/components/NpubQrCode'
import ProfileAbout from '@/components/ProfileAbout'
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'
@@ -25,6 +23,9 @@ import FollowedBy from './FollowedBy'
import Followings from './Followings'
import ProfileFeed from './ProfileFeed'
import Relays from './Relays'
import TextWithEmojis from '../TextWithEmojis'
import AvatarWithLightbox from './AvatarWithLightbox'
import BannerWithLightbox from './BannerWithLightbox'
export default function Profile({ id }: { id?: string }) {
const { t } = useTranslation()
@@ -114,12 +115,8 @@ export default function Profile({ id }: { id?: string }) {
<>
<div ref={topContainerRef}>
<div className="relative bg-cover bg-center mb-2">
<BannerWithLightbox banner={banner} pubkey={pubkey} className="w-full aspect-[3/1]" />
<AvatarWithLightbox
userId={pubkey}
size="large"
className="absolute left-3 bottom-0 translate-y-1/2 border-4 border-background"
/>
<BannerWithLightbox banner={banner} pubkey={pubkey} />
<AvatarWithLightbox userId={pubkey} />
</div>
<div className="px-4">
<div className="flex justify-end h-8 gap-2 items-center">
@@ -141,7 +138,11 @@ export default function Profile({ id }: { id?: string }) {
</div>
<div className="pt-2">
<div className="flex gap-2 items-center">
<div className="text-xl font-semibold truncate select-text">{username}</div>
<TextWithEmojis
text={username}
emojis={emojis}
className="text-xl font-semibold truncate select-text"
/>
{isFollowingYou && (
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
{t('Follows you')}

View File

@@ -1,11 +1,6 @@
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({
@@ -37,78 +32,3 @@ 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 (
<>
<Image
image={{ url: bannerUrl, pubkey }}
alt={`${pubkey} banner`}
className={cn('rounded-none', className)}
classNames={{
wrapper: 'cursor-zoom-in'
}}
errorPlaceholder={defaultBanner}
onClick={handleBannerClick}
/>
{index >= 0 &&
createPortal(
<div onClick={(e) => e.stopPropagation()}>
<Lightbox
index={index}
slides={[{ src: bannerUrl }]}
plugins={[Zoom]}
open={index >= 0}
close={() => setIndex(-1)}
controller={{
closeOnBackdropClick: true,
closeOnPullUp: true,
closeOnPullDown: true
}}
styles={{
toolbar: { paddingTop: '2.25rem' }
}}
/>
</div>,
document.body
)}
</>
)
}

View File

@@ -3,14 +3,9 @@ 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 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 { useMemo } from 'react'
import Image from '../Image'
import ProfileCard from '../ProfileCard'
@@ -84,71 +79,3 @@ 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<HTMLDivElement, MouseEvent>) => {
e.stopPropagation()
e.preventDefault()
setIndex(0)
}
const imageUrl = profile?.avatar ?? defaultAvatar
return (
<>
<SimpleUserAvatar
userId={userId}
size={size}
className={cn('cursor-zoom-in', className)}
onClick={handleClick}
/>
{index >= 0 &&
createPortal(
<div onClick={(e) => e.stopPropagation()}>
<Lightbox
index={index}
slides={[{ src: imageUrl }]}
plugins={[Zoom]}
open={index >= 0}
close={() => setIndex(-1)}
controller={{
closeOnBackdropClick: true,
closeOnPullUp: true,
closeOnPullDown: true
}}
styles={{
toolbar: { paddingTop: '2.25rem' }
}}
/>
</div>,
document.body
)}
</>
)
}

View File

@@ -73,10 +73,13 @@ class LightningService {
comment
})
const zapRequest = await client.signer.signEvent(zapRequestDraft)
const separator = callback.includes('?') ? '&' : '?'
const zapRequestRes = await fetch(
`${callback}${separator}amount=${amount}&nostr=${encodeURI(JSON.stringify(zapRequest))}&lnurl=${lnurl}`
)
const zapRequestUrl = new URL(callback)
zapRequestUrl.searchParams.append('amount', amount.toString())
zapRequestUrl.searchParams.append('nostr', JSON.stringify(zapRequest))
zapRequestUrl.searchParams.append('lnurl', lnurl)
const zapRequestRes = await fetch(zapRequestUrl.toString())
const zapRequestResBody = await zapRequestRes.json()
if (zapRequestResBody.error) {
throw new Error(zapRequestResBody.message)