fix: fake profile avatar position when resize
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
a4fefd9926
commit
3af93e3776
|
|
@ -1,9 +1,26 @@
|
|||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import * as React from "react"
|
||||
|
||||
import { useHotkeyScope, useTypeScriptHappyCallback } from "~/hooks/common"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenu: typeof DropdownMenuPrimitive.Root = (props) => {
|
||||
const [open, setOpen] = React.useState(!!props.open)
|
||||
useHotkeyScope("DropdownMenu", open)
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root
|
||||
{...props}
|
||||
onOpenChange={useTypeScriptHappyCallback(
|
||||
(open) => {
|
||||
setOpen(open)
|
||||
props.onOpenChange?.(open)
|
||||
},
|
||||
[props.onOpenChange],
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
|
|
@ -54,20 +71,22 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
|
|||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"shadow-context-menu z-50 min-w-32 overflow-hidden rounded-md border bg-theme-background p-1 text-theme-foreground",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"shadow-context-menu z-50 min-w-32 overflow-hidden rounded-md border bg-theme-background p-1 text-theme-foreground",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
})
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ export const HotKeyScopeMap = {
|
|||
Home: ["home"],
|
||||
Menu: ["menu"],
|
||||
Modal: ["modal"],
|
||||
DropdownMenu: ["dropdown-menu"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from "react"
|
||||
import { useCallback, useLayoutEffect, useRef } from "react"
|
||||
import { useHotkeysContext } from "react-hotkeys-hook"
|
||||
|
||||
import { HotKeyScopeMap } from "~/constants"
|
||||
|
|
@ -18,9 +18,41 @@ export const useSwitchHotKeyScope = () => {
|
|||
for (const key of allScopes) {
|
||||
disableScope(key)
|
||||
}
|
||||
|
||||
enableScope(nextScope[0])
|
||||
for (const key of nextScope) {
|
||||
enableScope(key)
|
||||
}
|
||||
},
|
||||
[disableScope, enableScope],
|
||||
)
|
||||
}
|
||||
export const useHotkeyScopeFn = (scope: keyof typeof HotKeyScopeMap) => {
|
||||
const { enableScope, disableScope, enabledScopes } = useHotkeysContext()
|
||||
const currentScopeRef = useRef(enabledScopes)
|
||||
|
||||
return useCallback(() => {
|
||||
const currentScope = currentScopeRef.current
|
||||
for (const key of allScopes) {
|
||||
disableScope(key)
|
||||
}
|
||||
for (const key of scope) {
|
||||
enableScope(key)
|
||||
}
|
||||
return () => {
|
||||
for (const key of scope) {
|
||||
disableScope(key)
|
||||
}
|
||||
for (const scope of currentScope) {
|
||||
enableScope(scope)
|
||||
}
|
||||
}
|
||||
}, [enableScope, disableScope, scope])
|
||||
}
|
||||
|
||||
export const useHotkeyScope = (scope: keyof typeof HotKeyScopeMap, when: boolean) => {
|
||||
const fn = useHotkeyScopeFn(scope)
|
||||
useLayoutEffect(() => {
|
||||
if (when) {
|
||||
return fn()
|
||||
}
|
||||
}, [fn, when])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { repository } from "@pkg"
|
||||
import type { FC } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { forwardRef, memo, useCallback, useLayoutEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
const presentUserProfile = usePresentUserProfileModal("dialog")
|
||||
const presentAchievement = useAchievementModal()
|
||||
const { t } = useTranslation()
|
||||
const [ref, { x, y }] = useMeasure()
|
||||
|
||||
const [dropdown, setDropdown] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -50,9 +50,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
<>
|
||||
<DropdownMenu onOpenChange={setDropdown}>
|
||||
<DropdownMenuTrigger asChild className="!outline-none focus-visible:bg-theme-item-hover">
|
||||
<ActionButton>
|
||||
<UserAvatar ref={ref} className="h-6 p-0 [&_*]:border-0" hideName />
|
||||
</ActionButton>
|
||||
<TransitionAvatar stage={dropdown ? "zoom-in" : ""} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
|
|
@ -134,26 +132,67 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{x !== 0 && y !== 0 && (
|
||||
</>
|
||||
)
|
||||
})
|
||||
ProfileButton.displayName = "ProfileButton"
|
||||
|
||||
const TransitionAvatar = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
stage: "zoom-in" | ""
|
||||
} & React.HTMLAttributes<HTMLButtonElement>
|
||||
>(({ stage, ...props }, forwardRef) => {
|
||||
const [ref, { x, y }, forceRefresh] = useMeasure()
|
||||
const [avatarHovered, setAvatarHovered] = useState(false)
|
||||
|
||||
const zoomIn = stage === "zoom-in"
|
||||
const [currentZoomIn, setCurrentZoomIn] = useState(false)
|
||||
useLayoutEffect(() => {
|
||||
if (zoomIn) {
|
||||
setCurrentZoomIn(true)
|
||||
}
|
||||
}, [zoomIn])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
{...props}
|
||||
ref={forwardRef}
|
||||
onMouseEnter={useCallback(() => {
|
||||
forceRefresh()
|
||||
setAvatarHovered(true)
|
||||
}, [forceRefresh])}
|
||||
onMouseLeave={useCallback(() => {
|
||||
setAvatarHovered(false)
|
||||
}, [])}
|
||||
>
|
||||
<UserAvatar ref={ref} className="h-6 p-0 [&_*]:border-0" hideName />
|
||||
</ActionButton>
|
||||
{x !== 0 && y !== 0 && (avatarHovered || zoomIn || currentZoomIn) && (
|
||||
<RootPortal>
|
||||
<UserAvatar
|
||||
style={{
|
||||
left: x - (dropdown ? 16 : 0),
|
||||
left: x - (zoomIn ? 16 : 0),
|
||||
top: y,
|
||||
}}
|
||||
className={cn(
|
||||
"pointer-events-none fixed -bottom-6 z-[999] p-0 duration-200 [&_*]:border-0",
|
||||
"pointer-events-none fixed -bottom-6 p-0 duration-200 [&_*]:border-0",
|
||||
"transform-gpu will-change-[left,top,height]",
|
||||
dropdown ? "h-14 " : "h-6",
|
||||
zoomIn ? "z-[999] h-14" : "z-[-1] h-6",
|
||||
)}
|
||||
hideName
|
||||
onTransitionEnd={() => {
|
||||
if (!zoomIn && currentZoomIn) {
|
||||
setCurrentZoomIn(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</RootPortal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
ProfileButton.displayName = "ProfileButton"
|
||||
|
||||
function PowerButton() {
|
||||
const wallet = useWallet()
|
||||
|
|
|
|||
|
|
@ -20,56 +20,67 @@ export const UserAvatar = forwardRef<
|
|||
hideName?: boolean
|
||||
userId?: string
|
||||
enableModal?: boolean
|
||||
style?: React.CSSProperties
|
||||
} & LoginProps
|
||||
>(({ className, avatarClassName, hideName, userId, enableModal, style, ...props }, ref) => {
|
||||
const { session, status } = useSession({
|
||||
enabled: !userId,
|
||||
})
|
||||
const presentUserProfile = usePresentUserProfileModal("drawer")
|
||||
} & LoginProps &
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{ className, avatarClassName, hideName, userId, enableModal, style, onClick, ...props },
|
||||
ref,
|
||||
) => {
|
||||
const { session, status } = useSession({
|
||||
enabled: !userId,
|
||||
})
|
||||
const presentUserProfile = usePresentUserProfileModal("drawer")
|
||||
|
||||
const profile = useAuthQuery(
|
||||
defineQuery(["profiles", userId], async () => {
|
||||
const res = await apiClient.profiles.$get({
|
||||
query: { id: userId! },
|
||||
})
|
||||
return res.data
|
||||
}),
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
)
|
||||
const profile = useAuthQuery(
|
||||
defineQuery(["profiles", userId], async () => {
|
||||
const res = await apiClient.profiles.$get({
|
||||
query: { id: userId! },
|
||||
})
|
||||
return res.data
|
||||
}),
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
)
|
||||
|
||||
if (!userId && status !== "authenticated") {
|
||||
return <LoginButton {...props} />
|
||||
}
|
||||
if (!userId && status !== "authenticated") {
|
||||
return <LoginButton {...props} />
|
||||
}
|
||||
|
||||
const renderUserData = userId ? profile.data : session?.user
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300",
|
||||
className,
|
||||
)}
|
||||
onClick={enableModal ? () => presentUserProfile(userId) : undefined}
|
||||
>
|
||||
<Avatar
|
||||
const renderUserData = userId ? profile.data : session?.user
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
if (enableModal) {
|
||||
presentUserProfile(userId)
|
||||
}
|
||||
onClick?.(e)
|
||||
}}
|
||||
{...props}
|
||||
className={cn(
|
||||
"aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300",
|
||||
avatarClassName,
|
||||
"flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<AvatarImage
|
||||
className="duration-200 animate-in fade-in-0"
|
||||
src={replaceImgUrlIfNeed(renderUserData?.image || undefined)}
|
||||
/>
|
||||
<AvatarFallback>{renderUserData?.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{!hideName && <div>{renderUserData?.name}</div>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
<Avatar
|
||||
className={cn(
|
||||
"aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300",
|
||||
avatarClassName,
|
||||
)}
|
||||
>
|
||||
<AvatarImage
|
||||
className="duration-200 animate-in fade-in-0"
|
||||
src={replaceImgUrlIfNeed(renderUserData?.image || undefined)}
|
||||
/>
|
||||
<AvatarFallback>{renderUserData?.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{!hideName && <div>{renderUserData?.name}</div>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
UserAvatar.displayName = "UserAvatar"
|
||||
|
|
|
|||
Loading…
Reference in New Issue