diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index a241bf47..972cdfc9 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -2,14 +2,17 @@ import type { GameShop } from "@types"; import Color from "color"; -export const formatDownloadProgress = (progress?: number) => { +export const formatDownloadProgress = ( + progress?: number, + fractionDigits?: number +) => { if (!progress) return "0%"; const progressPercentage = progress * 100; - if (Number(progressPercentage.toFixed(2)) % 1 === 0) + if (Number(progressPercentage.toFixed(fractionDigits ?? 2)) % 1 === 0) return `${Math.floor(progressPercentage)}%`; - return `${progressPercentage.toFixed(2)}%`; + return `${progressPercentage.toFixed(fractionDigits ?? 2)}%`; }; export const getSteamLanguage = (language: string) => { diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts index cc462a95..847e9492 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts +++ b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts @@ -228,3 +228,11 @@ export const link = style({ cursor: "pointer", }, }); + +export const gameCardStats = style({ + width: "100%", + height: "100%", + transition: "transform 0.5s ease-in-out", + flexShrink: "0", + flexGrow: "0", +}); diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index ed63029b..a8f65f0f 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -1,5 +1,5 @@ import { userProfileContext } from "@renderer/context"; -import { useCallback, useContext, useEffect, useMemo } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ProfileHero } from "../profile-hero/profile-hero"; import { useAppDispatch, useFormat } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; @@ -12,12 +12,16 @@ import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; -import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { UserStatsBox } from "./user-stats-box"; import { UserLibraryGameCard } from "./user-library-game-card"; +const GAME_STAT_ANIMATION_DURATION_IN_MS = 3500; + export function ProfileContent() { const { userProfile, isMe, userStats } = useContext(userProfileContext); + const [statsIndex, setStatsIndex] = useState(0); + const [isAnimationRunning, setIsAnimationRunning] = useState(true); + const statsAnimation = useRef(-1); const dispatch = useAppDispatch(); @@ -31,6 +35,35 @@ export function ProfileContent() { } }, [userProfile, dispatch]); + const handleOnMouseEnterGameCard = () => { + setIsAnimationRunning(false); + }; + + const handleOnMouseLeaveGameCard = () => { + setIsAnimationRunning(true); + }; + + useEffect(() => { + let zero = performance.now(); + if (!isAnimationRunning) return; + + statsAnimation.current = requestAnimationFrame( + function animateClosing(time) { + if (time - zero <= GAME_STAT_ANIMATION_DURATION_IN_MS) { + statsAnimation.current = requestAnimationFrame(animateClosing); + } else { + setStatsIndex((index) => index + 1); + zero = performance.now(); + statsAnimation.current = requestAnimationFrame(animateClosing); + } + } + ); + + return () => { + cancelAnimationFrame(statsAnimation.current); + }; + }, [setStatsIndex, isAnimationRunning]); + const { numberFormatter } = useFormat(); const navigate = useNavigate(); @@ -39,22 +72,6 @@ export function ProfileContent() { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); - const formatPlayTime = useCallback( - (playTimeInSeconds = 0) => { - const minutes = playTimeInSeconds / 60; - - if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t("amount_minutes", { - amount: minutes.toFixed(0), - }); - } - - const hours = minutes / 60; - return t("amount_hours", { amount: numberFormatter.format(hours) }); - }, - [numberFormatter, t] - ); - const content = useMemo(() => { if (!userProfile) return null; @@ -101,7 +118,13 @@ export function ProfileContent() { @@ -125,8 +148,8 @@ export function ProfileContent() { userStats, numberFormatter, t, - formatPlayTime, navigate, + statsIndex, ]); return ( diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index d35a4d30..b61935f5 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -3,7 +3,7 @@ import * as styles from "./profile-content.css"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useFormat } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useContext } from "react"; import { buildGameAchievementPath, buildGameDetailsPath, @@ -18,43 +18,27 @@ import { steamUrlBuilder } from "@shared"; interface UserLibraryGameCardProps { game: UserGame; + statIndex: number; + onMouseEnter: () => void; + onMouseLeave: () => void; } -export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) { +export function UserLibraryGameCard({ + game, + statIndex, + onMouseEnter, + onMouseLeave, +}: UserLibraryGameCardProps) { const { userProfile } = useContext(userProfileContext); const { t } = useTranslation("user_profile"); const { numberFormatter } = useFormat(); const navigate = useNavigate(); - const [mediaIndex, setMediaIndex] = useState(0); - - const statsItemCount = - Number(Boolean(game.achievementsPointsEarnedSum)) + - Number(Boolean(game.unlockedAchievementCount)); - - console.log(game.title, statsItemCount); - - useEffect(() => { - if (statsItemCount <= 1) return; - - let zero = performance.now(); - const animation = requestAnimationFrame(function animateClosing(time) { - if (time - zero <= 4000) { - requestAnimationFrame(animateClosing); - } else { - setMediaIndex((index) => { - if (index === statsItemCount - 1) return 0; - return index + 1; - }); - zero = performance.now(); - requestAnimationFrame(animateClosing); - } - }); - - return () => { - cancelAnimationFrame(animation); - }; - }, [setMediaIndex, statsItemCount]); + const getStatsItemCount = useCallback(() => { + let statsCount = 1; + if (game.achievementsPointsEarnedSum > 0) statsCount++; + return statsCount; + }, [game]); const buildUserGameDetailsPath = useCallback( (game: UserGame) => { @@ -76,6 +60,14 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) { [userProfile] ); + const formatAchievementPoints = (number: number) => { + if (number < 100_000) return numberFormatter.format(number); + + if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`; + + return `${(number / 1_000_000).toFixed(1)}M`; + }; + const formatPlayTime = useCallback( (playTimeInSeconds = 0) => { const minutes = playTimeInSeconds / 60; @@ -94,7 +86,8 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) { return (
  • @@ -147,33 +140,32 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) { style={{ width: "100%", display: "flex", - overflow: "hidden", + flexDirection: "column", }} >
    @@ -182,39 +174,37 @@ export function UserLibraryGameCard({ game }: UserLibraryGameCardProps) {
    - - {formatDownloadProgress( - game.unlockedAchievementCount / game.achievementCount - )} - + {game.achievementsPointsEarnedSum > 0 && ( +
    + + {formatAchievementPoints( + game.achievementsPointsEarnedSum + )} +
    + )}
    - + + {formatDownloadProgress( + game.unlockedAchievementCount / game.achievementCount, + 1 + )} +
    - {game.achievementsPointsEarnedSum > 0 && ( -
    - - {numberFormatter.format(game.achievementsPointsEarnedSum)} -
    - )} + )}