diff --git a/components/monitoring/AwsRumProvider.tsx b/components/monitoring/AwsRumProvider.tsx index ff85182dd2..4ee3c451d5 100644 --- a/components/monitoring/AwsRumProvider.tsx +++ b/components/monitoring/AwsRumProvider.tsx @@ -12,8 +12,6 @@ export default function AwsRumProvider({ children, }: Readonly) { useEffect(() => { - // Only initialize AWS RUM on the client side - if (typeof window === "undefined") return; // Skip initialization in development mode to avoid noise if (publicEnv.NODE_ENV === "development") { diff --git a/components/user/utils/profile/UserProfileTooltip.tsx b/components/user/utils/profile/UserProfileTooltip.tsx index 2d2a260b20..9848bb81df 100644 --- a/components/user/utils/profile/UserProfileTooltip.tsx +++ b/components/user/utils/profile/UserProfileTooltip.tsx @@ -4,6 +4,9 @@ import DropPfp from "@/components/drops/create/utils/DropPfp"; import { formatNumberWithCommasOrDash } from "@/helpers/Helpers"; import { useIdentity } from "@/hooks/useIdentity"; import { useIdentityBalance } from "@/hooks/useIdentityBalance"; +import UserFollowBtn, { + UserFollowBtnSize, +} from "@/components/user/utils/UserFollowBtn"; import UserCICTypeIcon from "../user-cic-type/UserCICTypeIcon"; import UserLevel from "../level/UserLevel"; import { CLASSIFICATIONS, CicStatement } from "@/entities/IProfile"; @@ -11,7 +14,8 @@ import { useQuery } from "@tanstack/react-query"; import { commonApiFetch } from "@/services/api/common-api"; import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper"; import { STATEMENT_GROUP, STATEMENT_TYPE } from "@/helpers/Types"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { AuthContext } from "@/components/auth/Auth"; export default function UserProfileTooltip({ user, @@ -53,31 +57,56 @@ export default function UserProfileTooltip({ ? CLASSIFICATIONS[profile.classification]?.title : null; + const { connectedProfile } = useContext(AuthContext); + const profileHandle = profile?.handle ?? null; + const normalizedProfileHandle = useMemo( + () => profileHandle?.toLowerCase() ?? null, + [profileHandle] + ); + const normalizedConnectedHandle = useMemo( + () => connectedProfile?.handle?.toLowerCase() ?? null, + [connectedProfile?.handle] + ); + const showFollowButton = Boolean( + normalizedProfileHandle && + normalizedProfileHandle !== normalizedConnectedHandle + ); + return (
-
-
- -
-
-
-
- - {profile?.handle ?? profile?.display} - - {profile && ( -
- +
+
+
+ +
+
+
+ + {profile?.handle ?? profile?.display} + + {profile && ( +
+ +
+ )}
- )} + {description && ( +

{description}

+ )} + {profile && ( +
+ +
+ )} +
- {description && ( -

{description}

- )} - {profile && ( -
- + {showFollowButton && profileHandle && ( +
+
)}
diff --git a/components/utils/tooltip/CustomTooltip.tsx b/components/utils/tooltip/CustomTooltip.tsx index 4a4b430726..1f816a21bf 100644 --- a/components/utils/tooltip/CustomTooltip.tsx +++ b/components/utils/tooltip/CustomTooltip.tsx @@ -19,6 +19,7 @@ interface CustomTooltipProps { readonly delayHide?: number; readonly disabled?: boolean; readonly offset?: number; + readonly hoverTransitionDelay?: number; } type TooltipChildHandlers = { @@ -40,6 +41,7 @@ export default function CustomTooltip({ delayHide = 0, disabled = false, offset = 8, + hoverTransitionDelay = 150, }: CustomTooltipProps) { const [isVisible, setIsVisible] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); @@ -54,6 +56,7 @@ export default function CustomTooltip({ const hideTimer = useRef | undefined>(undefined); const childObserverRef: MutableRefObject = useRef(null); const tooltipObserverRef: MutableRefObject = useRef(null); + const isPointerOverTooltipRef = useRef(false); const childElement = React.Children.only(children) as React.ReactElement; const originalRef = (childElement as React.ReactElement & { ref?: React.Ref; @@ -226,22 +229,45 @@ export default function CustomTooltip({ setActualPlacement(adjustedPosition.finalPlacement as "top" | "bottom" | "left" | "right"); }, [getOptimalPlacement, calculateInitialPosition, adjustPositionForViewport, calculateArrowPosition]); - const show = useCallback(() => { - if (disabled) return; + const cancelShowTimer = useCallback(() => { + if (showTimer.current !== undefined) { + clearTimeout(showTimer.current); + showTimer.current = undefined; + } + }, []); + + const cancelHideTimer = useCallback(() => { if (hideTimer.current !== undefined) { clearTimeout(hideTimer.current); + hideTimer.current = undefined; } + }, []); + + const show = useCallback(() => { + if (disabled) return; + cancelHideTimer(); showTimer.current = setTimeout(() => { setIsVisible(true); }, delayShow); - }, [disabled, delayShow]); + }, [disabled, delayShow, cancelHideTimer]); const hide = useCallback(() => { - if (showTimer.current !== undefined) { - clearTimeout(showTimer.current); + cancelShowTimer(); + if (isPointerOverTooltipRef.current) { + return; } - hideTimer.current = setTimeout(() => setIsVisible(false), delayHide); - }, [delayHide]); + hideTimer.current = setTimeout(() => setIsVisible(false), delayHide + hoverTransitionDelay); + }, [delayHide, hoverTransitionDelay, cancelShowTimer]); + + const handleTooltipMouseEnter = useCallback(() => { + isPointerOverTooltipRef.current = true; + cancelHideTimer(); + }, [cancelHideTimer]); + + const handleTooltipMouseLeave = useCallback(() => { + isPointerOverTooltipRef.current = false; + hide(); + }, [hide]); useLayoutEffect(() => { if (!isVisible) return; @@ -326,16 +352,12 @@ export default function CustomTooltip({ useEffect(() => { return () => { - if (showTimer.current !== undefined) { - clearTimeout(showTimer.current); - } - if (hideTimer.current !== undefined) { - clearTimeout(hideTimer.current); - } + cancelShowTimer(); + cancelHideTimer(); childObserverRef.current?.disconnect(); tooltipObserverRef.current?.disconnect(); }; - }, []); + }, [cancelShowTimer, cancelHideTimer]); const clonedChild = React.cloneElement( childElement, @@ -381,8 +403,10 @@ export default function CustomTooltip({ left: `${position.x}px`, top: `${position.y}px`, zIndex: 999999, - pointerEvents: 'none', + pointerEvents: 'auto', }} + onMouseEnter={handleTooltipMouseEnter} + onMouseLeave={handleTooltipMouseLeave} >
{content} diff --git a/components/waves/drops/WaveDropAuthorPfp.tsx b/components/waves/drops/WaveDropAuthorPfp.tsx index b0384384d9..95bb0284ec 100644 --- a/components/waves/drops/WaveDropAuthorPfp.tsx +++ b/components/waves/drops/WaveDropAuthorPfp.tsx @@ -2,8 +2,10 @@ import React from "react"; import Image from "next/image"; +import Link from "next/link"; import { ApiDrop } from "@/generated/models/ApiDrop"; import { resolveIpfsUrlSync } from "@/components/ipfs/IPFSContext"; +import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; interface WaveDropAuthorPfpProps { readonly drop: ApiDrop; @@ -11,28 +13,56 @@ interface WaveDropAuthorPfpProps { const WaveDropAuthorPfp: React.FC = ({ drop }) => { // Check if this drop author has any main stage winner drop IDs - const isFirstPlace = drop.author.winner_main_stage_drop_ids && - drop.author.winner_main_stage_drop_ids.length > 0; - const shadowClass = isFirstPlace ? "tw-shadow-[0_1px_4px_rgba(251,191,36,0.15)]" : ""; + const isFirstPlace = + drop.author.winner_main_stage_drop_ids && + drop.author.winner_main_stage_drop_ids.length > 0; + + const shadowClass = isFirstPlace + ? "tw-shadow-[0_1px_4px_rgba(251,191,36,0.15)]" + : ""; + const resolvedPfp = drop.author.pfp ? resolveIpfsUrlSync(drop.author.pfp) : null; + const authorHandle = drop.author.handle; + const profileHref = authorHandle ? `/${authorHandle}` : null; + const tooltipUser = authorHandle ?? drop.author.id; + + const containerClasses = `tw-relative tw-flex-shrink-0 tw-h-10 tw-w-10 tw-rounded-lg tw-bg-iron-900 tw-overflow-hidden ${shadowClass}`; + + const avatarContent = resolvedPfp ? ( + {authorHandle + ) : ( +
+ ); + + const handleClick = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + if (!profileHref) { + return
{avatarContent}
; + } + return ( -
- {resolvedPfp ? ( - Profile picture - ) : ( -
- )} -
+ + + {avatarContent} + + ); }; diff --git a/package.json b/package.json index 954ab5b74b..1cdb4bb9be 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "type-check": "tsc --noEmit -p tsconfig.json", "lint:quiet": "eslint . --quiet", "lint": "eslint .", + "lint:changed": "bash -lc '{ git diff --name-only -z main...HEAD -- \"*.js\" \"*.jsx\" \"*.ts\" \"*.tsx\" \":(exclude)generated/**\"; git ls-files --others --exclude-standard -z -- \"*.js\" \"*.jsx\" \"*.ts\" \"*.tsx\" \":(exclude)generated/**\"; } | xargs -0 npx eslint --no-warn-ignored --max-warnings=0'", "lint:fix": "npx eslint . --ext .ts,.tsx,.js,.jsx --fix", "relative-to-alias-imports": "tsx scripts/relative-to-alias-imports.ts", "deadcode:knip": "knip",