UserPopover (#1094)

* WIP UserPopover

* Add show delay on UserPopover

* UserDetails -> StackingSince on UserPopover

* Make UserPopover hoverable

* Add felipe to contributors.txt

* Remove export from SocialLink

* Remove @ outside of UserPopover

* userQuery -> useLazyQuery + Handling user not found

* Move styles to user-popover.module.css

* Update components/user-popover.module.css

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Remove poll + SSR check from useLazyQuery

* USER_FULL -> USER (we are only using stacking since, for now)

* refine user popover

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
Felipe Bueno 2024-05-03 18:39:21 -03:00 committed by GitHub
parent cdeaa35ff4
commit 72c27e339c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 165 additions and 43 deletions

View File

@ -21,6 +21,7 @@ import MuteDropdownItem from './mute'
import { DropdownItemUpVote } from './upvote' import { DropdownItemUpVote } from './upvote'
import { useRoot } from './root' import { useRoot } from './root'
import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header' import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
import UserPopover from './user-popover'
export default function ItemInfo ({ export default function ItemInfo ({
item, full, commentsText = 'comments', item, full, commentsText = 'comments',
@ -101,10 +102,12 @@ export default function ItemInfo ({
</Link> </Link>
<span> \ </span> <span> \ </span>
<span> <span>
<Link href={`/${item.user.name}`}> <UserPopover name={item.user.name}>
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} /> <Link href={`/${item.user.name}`}>
{embellishUser} @{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
</Link> {embellishUser}
</Link>
</UserPopover>
<span> </span> <span> </span>
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning> <Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
{timeSince(new Date(item.createdAt))} {timeSince(new Date(item.createdAt))}

View File

@ -21,6 +21,7 @@ import { useRouter } from 'next/router'
import Link from 'next/link' import Link from 'next/link'
import { UNKNOWN_LINK_REL } from '@/lib/constants' import { UNKNOWN_LINK_REL } from '@/lib/constants'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import UserPopover from './user-popover'
export function SearchText ({ text }) { export function SearchText ({ text }) {
return ( return (
@ -196,7 +197,18 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
</Link> </Link>
) )
} }
if (href.startsWith('/') || url?.origin === internalURL) { if (text.startsWith('@')) {
return (
<UserPopover name={text.replace('@', '')}>
<Link
id={props.id}
href={href}
>
{text}
</Link>
</UserPopover>
)
} else if (href.startsWith('/') || url?.origin === internalURL) {
return ( return (
<Link <Link
id={props.id} id={props.id}

View File

@ -11,6 +11,7 @@ import Hat from './hat'
import { useMe } from './me' import { useMe } from './me'
import { MEDIA_URL } from '@/lib/constants' import { MEDIA_URL } from '@/lib/constants'
import { NymActionDropdown } from '@/components/user-header' import { NymActionDropdown } from '@/components/user-header'
import classNames from 'classnames'
// all of this nonsense is to show the stat we are sorting by first // all of this nonsense is to show the stat we are sorting by first
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>) const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
@ -39,7 +40,29 @@ function seperate (arr, seperator) {
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x]) return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
} }
function User ({ user, rank, statComps, Embellish, nymActionDropdown = false }) { export function UserBase ({ user, className, children, nymActionDropdown }) {
return (
<div className={classNames(styles.item, className)}>
<Link href={`/${user.name}`}>
<Image
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
className={`${userStyles.userimg} me-2`}
/>
</Link>
<div className={styles.hunk}>
<div className='d-flex'>
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
@{user.name}<Hat className='ms-1 fill-grey' height={14} width={14} user={user} />
</Link>
{nymActionDropdown && <NymActionDropdown user={user} className='' />}
</div>
{children}
</div>
</div>
)
}
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
const me = useMe() const me = useMe()
const showStatComps = statComps && statComps.length > 0 const showStatComps = statComps && statComps.length > 0
return ( return (
@ -50,27 +73,12 @@ function User ({ user, rank, statComps, Embellish, nymActionDropdown = false })
{rank} {rank}
</div>) </div>)
: <div />} : <div />}
<div className={`${styles.item} ${me?.id === user.id && me.privates?.hideFromTopUsers ? userStyles.hidden : 'mb-2'}`}> <UserBase user={user} nymActionDropdown={nymActionDropdown} className={(me?.id === user.id && me.privates?.hideFromTopUsers) ? userStyles.hidden : 'mb-2'}>
<Link href={`/${user.name}`}> {showStatComps &&
<Image <div className={styles.other}>
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32' {statComps.map((Comp, i) => <Comp key={i} user={user} />)}
className={`${userStyles.userimg} me-2`} </div>}
/> </UserBase>
</Link>
<div className={`${styles.hunk} ${!showStatComps && 'd-flex flex-column justify-content-around'}`}>
<div className='d-flex'>
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
@{user.name}<Hat className='ms-1 fill-grey' height={14} width={14} user={user} />
</Link>
{nymActionDropdown && <NymActionDropdown user={user} className='' />}
</div>
{showStatComps &&
<div className={styles.other}>
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
</div>}
{Embellish && <Embellish rank={rank} />}
</div>
</div>
</> </>
) )
} }
@ -152,23 +160,31 @@ export function UsersSkeleton () {
return ( return (
<div>{users.map((_, i) => ( <div>{users.map((_, i) => (
<div className={`${styles.item} ${styles.skeleton} mb-2`} key={i}> <UserSkeleton key={i} className='mb-2'>
<Image <div className={styles.other}>
src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/clouds.jpeg`} <span className={`${styles.otherItem} clouds`} />
width='32' height='32' <span className={`${styles.otherItem} clouds`} />
className={`${userStyles.userimg} clouds me-2`} <span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
/> <span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
<div className={styles.hunk}>
<div className={`${styles.name} clouds text-reset`} />
<div className={styles.other}>
<span className={`${styles.otherItem} clouds`} />
<span className={`${styles.otherItem} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
</div>
</div> </div>
</div> </UserSkeleton>
))} ))}
</div> </div>
) )
} }
export function UserSkeleton ({ children, className }) {
return (
<div className={`${styles.item} ${styles.skeleton} ${className}`}>
<Image
src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/clouds.jpeg`}
width='32' height='32'
className={`${userStyles.userimg} clouds me-2`}
/>
<div className={styles.hunk}>
<div className={`${styles.name} clouds text-reset`} />
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,82 @@
import { USER } from '@/fragments/users'
import errorStyles from '@/styles/error.module.css'
import { useLazyQuery } from '@apollo/client'
import Link from 'next/link'
import { useRef, useState } from 'react'
import { Popover } from 'react-bootstrap'
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
import { UserBase, UserSkeleton } from './user-list'
import styles from './user-popover.module.css'
import classNames from 'classnames'
function StackingSince ({ since }) {
return (
<small className='text-muted d-flex-inline'>
stacking since:{' '}
{since
? <Link href={`/items/${since}`}>#{since}</Link>
: <span>never</span>}
</small>
)
}
export default function UserPopover ({ name, children }) {
const [showOverlay, setShowOverlay] = useState(false)
const [getUser, { loading, data }] = useLazyQuery(
USER,
{
variables: { name },
fetchPolicy: 'cache-first'
}
)
const timeoutId = useRef(null)
const handleMouseEnter = () => {
clearTimeout(timeoutId.current)
getUser()
timeoutId.current = setTimeout(() => {
setShowOverlay(true)
}, 500)
}
const handleMouseLeave = () => {
clearTimeout(timeoutId.current)
timeoutId.current = setTimeout(() => setShowOverlay(false), 100)
}
return (
<OverlayTrigger
show={showOverlay}
placement='bottom'
onHide={handleMouseLeave}
overlay={
<Popover
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={styles.userPopover}
>
<Popover.Body className={styles.userPopBody}>
{!data || loading
? <UserSkeleton />
: !data.user
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
: (
<UserBase user={data.user} className='mb-0 pb-0'>
<StackingSince since={data.user.since} />
</UserBase>
)}
</Popover.Body>
</Popover>
}
>
<span
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</span>
</OverlayTrigger>
)
}

View File

@ -0,0 +1,8 @@
.userPopover {
border: 1px solid var(--theme-toolbarActive)
}
.userPopBody {
font-weight: 500;
font-size: 0.9rem;
}

View File

@ -7,4 +7,5 @@ bitcoinplebdev
benthecarman benthecarman
stargut stargut
mz mz
btcbagehot btcbagehot
felipe