mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
Merge pull request #1354 from hydralauncher/feat/game-card-animation
feat: game card stats animation
This commit is contained in:
commit
3bef2633fd
@ -244,7 +244,7 @@ export class DownloadManager {
|
|||||||
private static async getDownloadPayload(game: Game) {
|
private static async getDownloadPayload(game: Game) {
|
||||||
switch (game.downloader) {
|
switch (game.downloader) {
|
||||||
case Downloader.Gofile: {
|
case Downloader.Gofile: {
|
||||||
const id = game!.uri!.split("/").pop();
|
const id = game.uri!.split("/").pop();
|
||||||
|
|
||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
@ -258,7 +258,7 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = game!.uri!.split("/").pop();
|
const id = game.uri!.split("/").pop();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
@ -2,14 +2,17 @@ import type { GameShop } from "@types";
|
|||||||
|
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
|
||||||
export const formatDownloadProgress = (progress?: number) => {
|
export const formatDownloadProgress = (
|
||||||
|
progress?: number,
|
||||||
|
fractionDigits?: number
|
||||||
|
) => {
|
||||||
if (!progress) return "0%";
|
if (!progress) return "0%";
|
||||||
const progressPercentage = progress * 100;
|
const progressPercentage = progress * 100;
|
||||||
|
|
||||||
if (Number(progressPercentage.toFixed(2)) % 1 === 0)
|
if (Number(progressPercentage.toFixed(fractionDigits ?? 2)) % 1 === 0)
|
||||||
return `${Math.floor(progressPercentage)}%`;
|
return `${Math.floor(progressPercentage)}%`;
|
||||||
|
|
||||||
return `${progressPercentage.toFixed(2)}%`;
|
return `${progressPercentage.toFixed(fractionDigits ?? 2)}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSteamLanguage = (language: string) => {
|
export const getSteamLanguage = (language: string) => {
|
||||||
|
@ -228,3 +228,11 @@ export const link = style({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const gameCardStats = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
transition: "transform 0.5s ease-in-out",
|
||||||
|
flexShrink: "0",
|
||||||
|
flexGrow: "0",
|
||||||
|
});
|
||||||
|
@ -1,31 +1,27 @@
|
|||||||
import { userProfileContext } from "@renderer/context";
|
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 { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
import * as styles from "./profile-content.css";
|
||||||
import { ClockIcon, TelescopeIcon, TrophyIcon } from "@primer/octicons-react";
|
import { TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
import { ReportProfile } from "../report-profile/report-profile";
|
import { ReportProfile } from "../report-profile/report-profile";
|
||||||
import { FriendsBox } from "./friends-box";
|
import { FriendsBox } from "./friends-box";
|
||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import type { UserGame } from "@types";
|
|
||||||
import {
|
|
||||||
buildGameAchievementPath,
|
|
||||||
buildGameDetailsPath,
|
|
||||||
formatDownloadProgress,
|
|
||||||
} from "@renderer/helpers";
|
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
|
||||||
import { UserStatsBox } from "./user-stats-box";
|
import { UserStatsBox } from "./user-stats-box";
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||||
|
|
||||||
|
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||||
|
const [statsIndex, setStatsIndex] = useState(0);
|
||||||
|
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||||
|
const statsAnimation = useRef(-1);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -39,6 +35,35 @@ export function ProfileContent() {
|
|||||||
}
|
}
|
||||||
}, [userProfile, dispatch]);
|
}, [userProfile, dispatch]);
|
||||||
|
|
||||||
|
const handleOnMouseEnterGameCard = () => {
|
||||||
|
setIsAnimationRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnMouseLeaveGameCard = () => {
|
||||||
|
setIsAnimationRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let zero = performance.now();
|
||||||
|
if (!isAnimationRunning) return;
|
||||||
|
|
||||||
|
statsAnimation.current = requestAnimationFrame(
|
||||||
|
function animateGameStats(time) {
|
||||||
|
if (time - zero <= GAME_STATS_ANIMATION_DURATION_IN_MS) {
|
||||||
|
statsAnimation.current = requestAnimationFrame(animateGameStats);
|
||||||
|
} else {
|
||||||
|
setStatsIndex((index) => index + 1);
|
||||||
|
zero = performance.now();
|
||||||
|
statsAnimation.current = requestAnimationFrame(animateGameStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(statsAnimation.current);
|
||||||
|
};
|
||||||
|
}, [setStatsIndex, isAnimationRunning]);
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -47,42 +72,6 @@ export function ProfileContent() {
|
|||||||
return userProfile?.relation?.status === "ACCEPTED";
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const buildUserGameDetailsPath = useCallback(
|
|
||||||
(game: UserGame) => {
|
|
||||||
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
|
||||||
return buildGameDetailsPath({
|
|
||||||
...game,
|
|
||||||
objectId: game.objectId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userParams = userProfile
|
|
||||||
? {
|
|
||||||
userId: userProfile.id,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return buildGameAchievementPath({ ...game }, userParams);
|
|
||||||
},
|
|
||||||
[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(() => {
|
const content = useMemo(() => {
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
||||||
@ -129,137 +118,13 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
<ul className={styles.gamesGrid}>
|
<ul className={styles.gamesGrid}>
|
||||||
{userProfile?.libraryGames?.map((game) => (
|
{userProfile?.libraryGames?.map((game) => (
|
||||||
<li
|
<UserLibraryGameCard
|
||||||
|
game={game}
|
||||||
key={game.objectId}
|
key={game.objectId}
|
||||||
style={{
|
statIndex={statsIndex}
|
||||||
borderRadius: 4,
|
onMouseEnter={handleOnMouseEnterGameCard}
|
||||||
overflow: "hidden",
|
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
title={game.title}
|
|
||||||
className={styles.game}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
className={styles.gameCover}
|
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
background:
|
|
||||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
|
|
||||||
padding: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
style={{
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
color: vars.color.muted,
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
borderRadius: 4,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 4,
|
|
||||||
padding: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ClockIcon size={11} />
|
|
||||||
{formatPlayTime(game.playTimeInSeconds)}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
{userProfile.hasActiveSubscription &&
|
|
||||||
game.achievementCount > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "#fff",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "start",
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HydraIcon width={16} height={16} />
|
|
||||||
{numberFormatter.format(
|
|
||||||
game.achievementsPointsEarnedSum
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
|
||||||
<span>
|
|
||||||
{game.unlockedAchievementCount} /{" "}
|
|
||||||
{game.achievementCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{formatDownloadProgress(
|
|
||||||
game.unlockedAchievementCount /
|
|
||||||
game.achievementCount
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<progress
|
|
||||||
max={1}
|
|
||||||
value={
|
|
||||||
game.unlockedAchievementCount /
|
|
||||||
game.achievementCount
|
|
||||||
}
|
|
||||||
className={styles.achievementsProgressBar}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img
|
|
||||||
src={steamUrlBuilder.cover(game.objectId)}
|
|
||||||
alt={game.title}
|
|
||||||
style={{
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: 4,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
minWidth: "100%",
|
|
||||||
minHeight: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@ -271,7 +136,6 @@ export function ProfileContent() {
|
|||||||
<UserStatsBox />
|
<UserStatsBox />
|
||||||
<RecentGamesBox />
|
<RecentGamesBox />
|
||||||
<FriendsBox />
|
<FriendsBox />
|
||||||
|
|
||||||
<ReportProfile />
|
<ReportProfile />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -284,9 +148,8 @@ export function ProfileContent() {
|
|||||||
userStats,
|
userStats,
|
||||||
numberFormatter,
|
numberFormatter,
|
||||||
t,
|
t,
|
||||||
buildUserGameDetailsPath,
|
|
||||||
formatPlayTime,
|
|
||||||
navigate,
|
navigate,
|
||||||
|
statsIndex,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -0,0 +1,227 @@
|
|||||||
|
import { UserGame } from "@types";
|
||||||
|
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 } from "react";
|
||||||
|
import {
|
||||||
|
buildGameAchievementPath,
|
||||||
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
|
interface UserLibraryGameCardProps {
|
||||||
|
game: UserGame;
|
||||||
|
statIndex: number;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
onMouseLeave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserLibraryGameCard({
|
||||||
|
game,
|
||||||
|
statIndex,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
}: UserLibraryGameCardProps) {
|
||||||
|
const { userProfile } = useContext(userProfileContext);
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
const { numberFormatter } = useFormat();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const getStatsItemCount = useCallback(() => {
|
||||||
|
let statsCount = 1;
|
||||||
|
if (game.achievementsPointsEarnedSum > 0) statsCount++;
|
||||||
|
return statsCount;
|
||||||
|
}, [game]);
|
||||||
|
|
||||||
|
const buildUserGameDetailsPath = useCallback(
|
||||||
|
(game: UserGame) => {
|
||||||
|
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
||||||
|
return buildGameDetailsPath({
|
||||||
|
...game,
|
||||||
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userParams = userProfile
|
||||||
|
? {
|
||||||
|
userId: userProfile.id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return buildGameAchievementPath({ ...game }, userParams);
|
||||||
|
},
|
||||||
|
[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;
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
title={game.title}
|
||||||
|
className={styles.game}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
className={styles.gameCover}
|
||||||
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
background:
|
||||||
|
"linear-gradient(0deg, rgba(0, 0, 0, 0.70) 20%, transparent 100%)",
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
style={{
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
color: vars.color.muted,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
borderRadius: 4,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClockIcon size={11} />
|
||||||
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 8,
|
||||||
|
color: vars.color.muted,
|
||||||
|
overflow: "hidden",
|
||||||
|
height: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.gameCardStats}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{game.unlockedAchievementCount} / {game.achievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{game.achievementsPointsEarnedSum > 0 && (
|
||||||
|
<div
|
||||||
|
className={styles.gameCardStats}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 5,
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HydraIcon width={16} height={16} />
|
||||||
|
{formatAchievementPoints(
|
||||||
|
game.achievementsPointsEarnedSum
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
game.unlockedAchievementCount / game.achievementCount,
|
||||||
|
1
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={game.unlockedAchievementCount / game.achievementCount}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.cover(game.objectId)}
|
||||||
|
alt={game.title}
|
||||||
|
style={{
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 4,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
minWidth: "100%",
|
||||||
|
minHeight: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user