diff --git a/components/drops/view/utils/DropVoteProgressing.tsx b/components/drops/view/utils/DropVoteProgressing.tsx index 8d52a792ce..be94aeb86d 100644 --- a/components/drops/view/utils/DropVoteProgressing.tsx +++ b/components/drops/view/utils/DropVoteProgressing.tsx @@ -27,22 +27,22 @@ export default function DropVoteProgressing({ const isPositiveProgressing = current < projected; - // Define styles based on subtle mode - let color; + let color: string; + let arrowColor: string; if (subtle) { - color = isPositiveProgressing - ? "tw-text-iron-300 tw-bg-iron-700 tw-transition-colors tw-duration-200" - : "tw-text-iron-400 tw-bg-iron-700 tw-transition-colors tw-duration-200"; + color = isPositiveProgressing ? "tw-text-iron-400 tw-font-mono" : "tw-text-iron-600 tw-font-mono"; + arrowColor = "tw-text-iron-600"; } else { color = isPositiveProgressing - ? "tw-text-emerald-400 tw-bg-emerald-900/40" - : "tw-text-rose-400 tw-bg-rose-900/40"; + ? "tw-text-emerald-400 tw-bg-emerald-900/40 tw-px-1.5 tw-py-0.5 tw-rounded-md" + : "tw-text-rose-400 tw-bg-rose-900/40 tw-px-1.5 tw-py-0.5 tw-rounded-md"; + arrowColor = isPositiveProgressing ? "tw-text-emerald-400" : "tw-text-rose-400"; } return ( <> - {formatNumberWithCommas(projected)} + {formatNumberWithCommas(projected)} - - Projected vote count at decision time:{" "} - {formatNumberWithCommas(projected)} - + Projected vote count at decision time:{" "} + {formatNumberWithCommas(projected)} ); diff --git a/components/voting/VotingModalButton.tsx b/components/voting/VotingModalButton.tsx index a8766b0b42..4fafc2e520 100644 --- a/components/voting/VotingModalButton.tsx +++ b/components/voting/VotingModalButton.tsx @@ -24,17 +24,17 @@ const VotingModalButton: React.FC = ({ onClick(); }; - const baseClasses = "tw-flex tw-items-center tw-justify-center tw-gap-x-1.5 tw-px-3 tw-py-1.5 tw-rounded-md tw-border tw-border-solid tw-shadow-sm tw-text-xs tw-whitespace-nowrap tw-transition-all tw-duration-150 tw-ease-in-out active:tw-transform active:tw-translate-y-0.5 active:tw-shadow-sm"; - + const baseClasses = "tw-flex tw-items-center tw-justify-center tw-gap-x-1 tw-px-3 tw-py-1.5 tw-rounded-lg tw-border tw-border-solid tw-text-xs tw-whitespace-nowrap tw-transition-all tw-duration-300 tw-ease-out"; + const variantClasses = variant === "subtle" - ? "tw-bg-iron-900 tw-border-iron-700 tw-text-iron-200 tw-font-medium hover:tw-text-white hover:tw-bg-primary-600 hover:tw-border-primary-600 hover:tw-shadow-md group" - : "tw-bg-primary-500 tw-border-primary-500 tw-text-white tw-font-semibold hover:tw-bg-primary-600 hover:tw-shadow-md"; - + ? "tw-bg-white tw-border-white tw-text-black tw-font-semibold desktop-hover:hover:tw-bg-iron-300 tw-shadow-[0_0_15px_rgba(255,255,255,0.1)]" + : "tw-bg-primary-500 tw-border-primary-500 tw-text-white tw-font-medium desktop-hover:hover:tw-bg-primary-600"; + const buttonClasses = `${baseClasses} ${variantClasses}`; return ( ); }; diff --git a/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem.tsx b/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem.tsx index 3ae904d403..50514ed409 100644 --- a/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem.tsx +++ b/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, memo } from "react"; +import React, { useState, useEffect, useRef, memo } from "react"; import { ExtendedDrop } from "@/helpers/waves/drop.helpers"; import MediaDisplay from "@/components/drops/view/item/content/media/MediaDisplay"; import WaveLeaderboardGalleryItemVotes from "./WaveLeaderboardGalleryItemVotes"; @@ -18,7 +18,7 @@ import { WaveDropsLeaderboardSort } from "@/hooks/useWaveDropsLeaderboard"; interface WaveLeaderboardGalleryItemProps { readonly drop: ExtendedDrop; readonly onDropClick: (drop: ExtendedDrop) => void; - readonly artFocused?: boolean; // New prop to activate art-focused mode + readonly artFocused?: boolean; readonly activeSort?: WaveDropsLeaderboardSort; readonly animationKey?: number; } @@ -28,50 +28,46 @@ export const WaveLeaderboardGalleryItem = memo( const [isVotingModalOpen, setIsVotingModalOpen] = useState(false); const [isHighlighting, setIsHighlighting] = useState(false); const isMobileScreen = useIsMobileScreen(); - const { hasTouchScreen } = useDeviceInfo(); // Detect touch devices + const { hasTouchScreen } = useDeviceInfo(); const { canShowVote } = useDropInteractionRules(drop); - // Animation state and useEffect - ONLY for desktop (non-touch) devices - const [isFirstRender, setIsFirstRender] = useState(true); - const [previousSort, setPreviousSort] = useState(activeSort); + const isFirstRenderRef = useRef(true); + const previousSortRef = useRef(activeSort); + const timerRef = useRef(null); useEffect(() => { - // Skip all animations on touch devices for performance if (hasTouchScreen) { return; } - let timer: NodeJS.Timeout | null = null; + const sortChanged = previousSortRef.current !== activeSort; + const shouldAnimate = + (isFirstRenderRef.current && animationKey > 0) || + (!isFirstRenderRef.current && sortChanged); - // Skip animation on first render unless animationKey is set - if (isFirstRender) { - setIsFirstRender(false); - setPreviousSort(activeSort); + isFirstRenderRef.current = false; + previousSortRef.current = activeSort; - // Animate on mount if animationKey is set (indicating a sort change) - if (animationKey > 0) { - setIsHighlighting(true); - timer = setTimeout(() => { - setIsHighlighting(false); - }, 700); - } - } else if (previousSort !== activeSort) { - // Only animate if sort actually changed - setPreviousSort(activeSort); - setIsHighlighting(true); - timer = setTimeout(() => { - setIsHighlighting(false); - }, 700); + if (!shouldAnimate) { + return; + } + + setIsHighlighting(true); + if (timerRef.current) { + clearTimeout(timerRef.current); } + timerRef.current = setTimeout(() => { + setIsHighlighting(false); + }, 700); return () => { - if (timer) { - clearTimeout(timer); + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; } }; - }, [activeSort, animationKey, isFirstRender, previousSort, hasTouchScreen]); + }, [activeSort, animationKey, hasTouchScreen]); - // Consider the user has voted even if the rating is 0 const hasUserVoted = drop.context_profile_context?.rating !== undefined; const userVote = drop.context_profile_context?.rating ?? 0; @@ -83,11 +79,9 @@ export const WaveLeaderboardGalleryItem = memo( artFocused: boolean ) => { if (artFocused) { - // Subtle styling for art-focused mode if (isZero) return "tw-text-iron-400"; return isNegative ? "tw-text-iron-400" : "tw-text-iron-300"; } - // Original styling if (isZero) return "tw-text-iron-400"; return isNegative ? "tw-text-rose-500" : "tw-text-emerald-500"; }; @@ -99,48 +93,33 @@ export const WaveLeaderboardGalleryItem = memo( onDropClick(drop); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - onDropClick(drop); - } - }; - const handleVoteButtonClick = () => { setIsVotingModalOpen(true); }; - // Determine container class based on art-focused mode and highlighting state const transitionClasses = !hasTouchScreen ? "tw-transition-all tw-duration-300 tw-ease-out" : ""; - const groupClasses = artFocused ? `group ${transitionClasses}` : ""; - const containerClass = `${groupClasses} tw-relative tw-rounded-lg`; + const groupClasses = artFocused ? `tw-group ${transitionClasses}` : ""; + const containerClass = `${groupClasses} tw-relative tw-bg-iron-950/50 tw-border tw-border-solid tw-border-iron-800 tw-rounded-lg desktop-hover:hover:tw-border-iron-700 tw-shadow-lg desktop-hover:hover:tw-shadow-xl`; - // Apply image effects with animation when highlighting - ONLY on desktop const highlightAnimation = isHighlighting && !hasTouchScreen ? "tw-animate-gallery-reveal" : ""; - const baseImageClasses = "tw-aspect-square tw-border tw-border-iron-800 tw-relative tw-cursor-pointer touch-none"; - - const artFocusedHoverClasses = !hasTouchScreen - ? `desktop-hover:hover:tw-border-iron-700 tw-transform tw-duration-300 tw-ease-out tw-ring-0 desktop-hover:hover:tw-ring-1 desktop-hover:hover:tw-ring-iron-600 ${highlightAnimation}` - : ""; - - const defaultHoverClasses = !hasTouchScreen - ? `desktop-hover:hover:-tw-translate-y-0.5 desktop-hover:hover:tw-scale-[1.02] tw-transform tw-duration-300 tw-ease-out ${highlightAnimation}` - : ""; + const baseImageClasses = "tw-aspect-square tw-relative tw-cursor-pointer tw-touch-none tw-overflow-hidden tw-bg-iron-900 tw-group/image"; - const imageContainerClass = artFocused - ? `tw-ml-0.5 ${baseImageClasses} tw-bg-iron-900 ${artFocusedHoverClasses}` - : `${baseImageClasses} tw-bg-iron-800 ${defaultHoverClasses}`; + const imageScaleClasses = hasTouchScreen + ? "" + : `tw-transform tw-duration-700 tw-ease-out group-hover/image:tw-scale-105 ${highlightAnimation}`; + + const imageContainerClass = baseImageClasses; return (
-
-
- {/* Rank badge aligned to the left */} -
- {drop.rank !== undefined ? ( - - ) : ( -
- )}{" "} - {/* Empty space if no rank */} -
- - {/* Author name aligned to the right */} -
- {drop.author?.handle ? ( +
+
+
+ {drop.title && ( +

+ {drop.title} +

+ )} + {drop.author?.handle && ( e.stopPropagation()} href={`/${drop.author?.handle}`} - className="tw-text-sm tw-truncate tw-no-underline tw-font-medium tw-text-iron-200 desktop-hover:hover:tw-text-opacity-80 desktop-hover:hover:tw-underline" + className="tw-text-xs tw-text-iron-400 tw-mt-0.5 tw-no-underline desktop-hover:hover:tw-underline desktop-hover:hover:tw-text-iron-300 tw-transition-colors tw-duration-150" > {drop.author?.handle} - ) : ( - - {" "} - )}
+ {drop.rank !== undefined && ( + + )}
-
+
- -
+
- + {formatNumberWithCommas(drop.raters_count)}
- -
-
- {hasUserVoted && ( -
-
- - Your vote: - - - {isNegativeVote && "-"} - {formatNumberWithCommas(Math.abs(userVote))}{" "} - - {drop.wave.voting_credit_type} - - -
-
- )} -
+
+ {hasUserVoted && ( + + Your vote: {isNegativeVote && "-"}{formatNumberWithCommas(Math.abs(userVote))} {drop.wave.voting_credit_type} + + )} {canShowVote && ( - +
+ +
)}
- {/* Voting modal */} {isMobileScreen ? ( ( )}
); - }, - (prevProps, nextProps) => { - // Force re-render when activeSort or animationKey changes - if ( - prevProps.activeSort !== nextProps.activeSort || - prevProps.animationKey !== nextProps.animationKey - ) { - return false; - } - - // Otherwise, use shallow comparison for other props - return ( - prevProps.drop.id === nextProps.drop.id && - prevProps.onDropClick === nextProps.onDropClick && - prevProps.artFocused === nextProps.artFocused - ); } ); diff --git a/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItemVotes.tsx b/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItemVotes.tsx index c831c4af0a..53e574c07b 100644 --- a/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItemVotes.tsx +++ b/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItemVotes.tsx @@ -15,28 +15,21 @@ export default function WaveLeaderboardGalleryItemVotes({ const current = drop.rating ?? 0; const isPositive = current >= 0; - // Determine color classes based on variant const getColorClass = () => { - if (variant === 'subtle') { - // More subtle coloring that doesn't draw focus from the artwork - return isPositive - ? "tw-text-iron-300" - : "tw-text-iron-400"; + if (variant === "subtle") { + return "tw-text-iron-100"; } - // Original bright coloring return isPositive ? "tw-text-emerald-500" : "tw-text-rose-500"; }; return ( -
- +
+ {formatNumberWithCommas(current)} -