Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions components/monitoring/AwsRumProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export default function AwsRumProvider({
children,
}: Readonly<AwsRumProviderProps>) {
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") {
Expand Down
71 changes: 50 additions & 21 deletions components/user/utils/profile/UserProfileTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ 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";
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,
Expand Down Expand Up @@ -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 (
<div className="tailwind-scope tw-bg-iron-950 tw-min-w-[280px] tw-max-w-[320px]">
<div className="tw-flex tw-flex-col tw-gap-y-2">
<div className="tw-flex tw-justify-start">
<div className="tw-flex-shrink-0">
<DropPfp pfpUrl={profile?.pfp} />
</div>
</div>
<div className="tw-flex tw-flex-col">
<div className="tw-flex tw-items-center tw-gap-x-2">
<span className="tw-text-base tw-font-bold tw-text-iron-50 tw-truncate tw-max-w-[180px]">
{profile?.handle ?? profile?.display}
</span>
{profile && (
<div className="tw-h-5 tw-w-5">
<UserCICTypeIcon cic={profile.cic} />
<div className="tw-flex tw-items-start tw-justify-between tw-gap-x-4">
<div className="tw-flex tw-gap-x-3 tw-flex-1 tw-min-w-0">
<div className="tw-flex-shrink-0">
<DropPfp pfpUrl={profile?.pfp} />
</div>
<div className="tw-flex tw-flex-col tw-min-w-0">
<div className="tw-flex tw-items-center tw-gap-x-2">
<span className="tw-text-base tw-font-bold tw-text-iron-50 tw-truncate tw-max-w-[180px]">
{profile?.handle ?? profile?.display}
</span>
{profile && (
<div className="tw-h-5 tw-w-5">
<UserCICTypeIcon cic={profile.cic} />
</div>
)}
</div>
)}
{description && (
<p className="tw-text-xs tw-text-iron-400 tw-mb-0">{description}</p>
)}
{profile && (
<div className="tw-mt-1.5">
<UserLevel level={profile.level} size="xs" />
</div>
)}
</div>
</div>
{description && (
<p className="tw-text-xs tw-text-iron-400 tw-mb-0">{description}</p>
)}
{profile && (
<div className="tw-mt-1.5">
<UserLevel level={profile.level} size="xs" />
{showFollowButton && profileHandle && (
<div className="tw-flex-shrink-0">
<UserFollowBtn
handle={profileHandle}
size={UserFollowBtnSize.SMALL}
/>
</div>
)}
</div>
Expand Down
54 changes: 39 additions & 15 deletions components/utils/tooltip/CustomTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface CustomTooltipProps {
readonly delayHide?: number;
readonly disabled?: boolean;
readonly offset?: number;
readonly hoverTransitionDelay?: number;
}

type TooltipChildHandlers = {
Expand All @@ -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 });
Expand All @@ -54,6 +56,7 @@ export default function CustomTooltip({
const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const childObserverRef: MutableRefObject<ResizeObserver | null> = useRef(null);
const tooltipObserverRef: MutableRefObject<ResizeObserver | null> = useRef(null);
const isPointerOverTooltipRef = useRef(false);
const childElement = React.Children.only(children) as React.ReactElement<TooltipChildHandlers>;
const originalRef = (childElement as React.ReactElement & {
ref?: React.Ref<HTMLElement>;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
>
<div className={styles.tooltipContent}>
{content}
Expand Down
64 changes: 47 additions & 17 deletions components/waves/drops/WaveDropAuthorPfp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,67 @@

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;
}

const WaveDropAuthorPfp: React.FC<WaveDropAuthorPfpProps> = ({ 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 ? (
<Image
src={resolvedPfp}
alt={authorHandle ? `${authorHandle}'s profile picture` : "Profile picture"}
fill
sizes="40px"
className="tw-object-contain tw-rounded-lg tw-bg-transparent"
/>
) : (
<div className="tw-h-full tw-w-full tw-bg-iron-900 tw-ring-1 tw-ring-inset tw-ring-white/10 tw-rounded-lg" />
);

const handleClick = (event: React.MouseEvent) => {
event.stopPropagation();
};

if (!profileHref) {
return <div className={containerClasses}>{avatarContent}</div>;
}

return (
<div
className={`tw-relative tw-flex-shrink-0 tw-h-10 tw-w-10 tw-rounded-lg tw-bg-iron-900 tw-overflow-hidden ${shadowClass}`}>
{resolvedPfp ? (
<Image
src={resolvedPfp}
alt="Profile picture"
fill
sizes="40px"
className="tw-object-contain tw-rounded-lg tw-bg-transparent"
/>
) : (
<div className="tw-h-full tw-w-full tw-bg-iron-900 tw-ring-1 tw-ring-inset tw-ring-white/10 tw-rounded-lg" />
)}
</div>
<UserProfileTooltipWrapper user={tooltipUser}>
<Link
href={profileHref}
prefetch={false}
aria-label={`View ${tooltipUser}'s profile`}
onClick={handleClick}
className={`${containerClasses} tw-block`}
>
{avatarContent}
</Link>
</UserProfileTooltipWrapper>
);
};

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading