feat: update achievements page

This commit is contained in:
Zamitto 2024-10-09 20:33:33 -03:00
parent 8fb31e0e64
commit fa026f82a6
9 changed files with 228 additions and 52 deletions

View file

@ -333,6 +333,8 @@
"your_friend_code": "Your friend code:" "your_friend_code": "Your friend code:"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked" "achievement_unlocked": "Achievement unlocked",
"user_achievements": "{{displayName}}'s Achievements",
"your_achievements": "Your Achievements"
} }
} }

View file

@ -335,6 +335,8 @@
"your_friend_code": "Seu código de amigo:" "your_friend_code": "Seu código de amigo:"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Conquista desbloqueada" "achievement_unlocked": "Conquista desbloqueada",
"your_achievements": "Suas Conquistas",
"user_achievements": "Conquistas de {{displayName}}"
} }
} }

View file

@ -83,9 +83,9 @@ export const getGameAchievements = async (
unlocked: false, unlocked: false,
unlockTime: null, unlockTime: null,
icongray, icongray,
}; } as GameAchievement;
}) })
.sort((a: GameAchievement, b: GameAchievement) => { .sort((a, b) => {
if (a.unlocked && !b.unlocked) return -1; if (a.unlocked && !b.unlocked) return -1;
if (!a.unlocked && b.unlocked) return 1; if (!a.unlocked && b.unlocked) return 1;
if (a.unlocked && b.unlocked) { if (a.unlocked && b.unlocked) {

View file

@ -53,7 +53,7 @@ const getPathFromCracker = (cracker: Cracker) => {
if (cracker === Cracker.onlineFix) { if (cracker === Cracker.onlineFix) {
return [ return [
{ {
folderPath: path.join(publicDocuments, Cracker.onlineFix), folderPath: path.join(publicDocuments, "OnlineFix"),
fileLocation: ["Stats", "Achievements.ini"], fileLocation: ["Stats", "Achievements.ini"],
}, },
]; ];

View file

@ -34,5 +34,20 @@ export const buildGameDetailsPath = (
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`; return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
}; };
export const buildGameAchievementPath = (
game: { shop: GameShop; objectId: string; title: string },
user?: { userId: string; displayName: string }
) => {
const searchParams = new URLSearchParams({
title: game.title,
shop: game.shop,
objectId: game.objectId,
userId: user?.userId || "",
displayName: user?.displayName || "",
});
return `/achievements/?${searchParams.toString()}`;
};
export const darkenColor = (color: string, amount: number, alpha: number = 1) => export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
new Color(color).darken(amount).alpha(alpha).toString(); new Color(color).darken(amount).alpha(alpha).toString();

View file

@ -0,0 +1,82 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
export const container = style({
width: "100%",
padding: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
});
export const header = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
width: "50%",
});
export const headerImage = style({
borderRadius: "4px",
objectFit: "cover",
cursor: "pointer",
height: "160px",
transition: "all ease 0.2s",
":hover": {
transform: "scale(1.05)",
},
});
export const list = style({
listStyle: "none",
margin: "0",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
padding: 0,
});
export const listItem = style({
display: "flex",
transition: "all ease 0.1s",
color: vars.color.muted,
width: "100%",
overflow: "hidden",
borderRadius: "4px",
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
textAlign: "left",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
textDecoration: "none",
},
});
export const listItemImage = recipe({
base: {
width: "54px",
height: "54px",
borderRadius: "4px",
objectFit: "cover",
},
variants: {
unlocked: {
false: {
filter: "grayscale(100%)",
},
},
},
});
export const achievementsProgressBar = style({
width: "100%",
height: "8px",
transition: "all ease 0.2s",
"::-webkit-progress-bar": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
});

View file

@ -1,9 +1,17 @@
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { useAppDispatch, useDate } from "@renderer/hooks"; import { useAppDispatch, useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css"; import { steamUrlBuilder } from "@shared";
import { GameAchievement, GameShop } from "@types"; import type { GameAchievement, GameShop } from "@types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "./achievements.css";
import {
buildGameDetailsPath,
formatDownloadProgress,
} from "@renderer/helpers";
import { TrophyIcon } from "@primer/octicons-react";
import { vars } from "@renderer/theme.css";
export function Achievement() { export function Achievement() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -11,8 +19,12 @@ export function Achievement() {
const shop = searchParams.get("shop"); const shop = searchParams.get("shop");
const title = searchParams.get("title"); const title = searchParams.get("title");
const userId = searchParams.get("userId"); const userId = searchParams.get("userId");
const displayName = searchParams.get("displayName");
const { t } = useTranslation("achievement");
const { format } = useDate(); const { format } = useDate();
const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -30,53 +42,109 @@ export function Achievement() {
useEffect(() => { useEffect(() => {
if (title) { if (title) {
dispatch(setHeaderTitle(title + " Achievements")); dispatch(setHeaderTitle(title));
} }
}, [dispatch, title]); }, [dispatch, title]);
return ( if (!objectId || !shop || !title) return null;
<div>
<h1>Achievement</h1>
const unlockedAchievementCount = achievements.filter(
(achievement) => achievement.unlocked
).length;
const totalAchievementCount = achievements.length;
const handleClickGame = () => {
navigate(
buildGameDetailsPath({
shop: shop as GameShop,
objectId,
title,
})
);
};
return (
<div className={styles.container}>
<div className={styles.header}>
<button onClick={handleClickGame}>
<img
src={steamUrlBuilder.cover(objectId)}
alt={title}
className={styles.headerImage}
/>
</button>
<div <div
style={{ style={{
width: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px`,
}} }}
> >
{achievements.map((achievement, index) => ( <h1>
{displayName
? t("user_achievements", {
displayName,
})
: t("your_achievements")}
</h1>
<div <div
key={index}
style={{ style={{
display: "flex", display: "flex",
flexDirection: "row", justifyContent: "space-between",
alignItems: "center", marginBottom: 8,
gap: `${SPACING_UNIT}px`, color: vars.color.muted,
}} }}
title={achievement.description}
> >
<img <div
style={{ style={{
height: "60px", display: "flex",
width: "60px", alignItems: "center",
filter: achievement.unlocked ? "none" : "grayscale(100%)", gap: 8,
}} }}
>
<TrophyIcon size={13} />
<span>
{unlockedAchievementCount} / {totalAchievementCount}
</span>
</div>
<span>
{formatDownloadProgress(
unlockedAchievementCount / totalAchievementCount
)}
</span>
</div>
<progress
max={1}
value={unlockedAchievementCount / totalAchievementCount}
className={styles.achievementsProgressBar}
/>
</div>
</div>
<ul className={styles.list}>
{achievements.map((achievement, index) => (
<li key={index} className={styles.listItem}>
<img
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
src={ src={
achievement.unlocked ? achievement.icon : achievement.icongray achievement.unlocked ? achievement.icon : achievement.icongray
} }
alt={achievement.displayName}
loading="lazy" loading="lazy"
/> />
<div> <div>
<p>{achievement.displayName}</p> <p>{achievement.displayName}</p>
<p>{achievement.description}</p> <p>{achievement.description}</p>
<small>
{achievement.unlockTime && format(achievement.unlockTime)} {achievement.unlockTime && format(achievement.unlockTime)}
</small>
</div> </div>
</div> </li>
))} ))}
</div> </ul>
</div> </div>
); );
} }

View file

@ -10,6 +10,7 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { howLongToBeatEntriesTable } from "@renderer/dexie";
import { SidebarSection } from "../sidebar-section/sidebar-section"; import { SidebarSection } from "../sidebar-section/sidebar-section";
import { buildGameAchievementPath } from "@renderer/helpers";
export function Sidebar() { export function Sidebar() {
const [howLongToBeat, setHowLongToBeat] = useState<{ const [howLongToBeat, setHowLongToBeat] = useState<{
@ -28,16 +29,6 @@ export function Sidebar() {
const { numberFormatter } = useFormat(); const { numberFormatter } = useFormat();
const buildGameAchievementPath = () => {
const urlParams = new URLSearchParams({
objectId: objectId!,
shop,
title: gameTitle,
});
return `/achievements?${urlParams.toString()}`;
};
useEffect(() => { useEffect(() => {
if (objectId) { if (objectId) {
setHowLongToBeat({ isLoading: true, data: null }); setHowLongToBeat({ isLoading: true, data: null });
@ -88,7 +79,11 @@ export function Sidebar() {
{achievements.slice(0, 4).map((achievement, index) => ( {achievements.slice(0, 4).map((achievement, index) => (
<li key={index}> <li key={index}>
<Link <Link
to={buildGameAchievementPath()} to={buildGameAchievementPath({
shop: shop,
objectId: objectId!,
title: gameTitle,
})}
className={styles.listItem} className={styles.listItem}
title={achievement.description} title={achievement.description}
> >
@ -116,7 +111,11 @@ export function Sidebar() {
<Link <Link
style={{ textAlign: "center" }} style={{ textAlign: "center" }}
to={buildGameAchievementPath()} to={buildGameAchievementPath({
shop: shop,
objectId: objectId!,
title: gameTitle,
})}
> >
{t("see_all_achievements")} {t("see_all_achievements")}
</Link> </Link>

View file

@ -16,7 +16,7 @@ import { FriendsBox } from "./friends-box";
import { RecentGamesBox } from "./recent-games-box"; import { RecentGamesBox } from "./recent-games-box";
import { UserGame } from "@types"; import { UserGame } from "@types";
import { import {
buildGameDetailsPath, buildGameAchievementPath,
formatDownloadProgress, formatDownloadProgress,
} from "@renderer/helpers"; } from "@renderer/helpers";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
@ -44,11 +44,19 @@ export function ProfileContent() {
return userProfile?.relation?.status === "ACCEPTED"; return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]); }, [userProfile]);
const buildUserGameDetailsPath = (game: UserGame) => const buildUserGameDetailsPath = (game: UserGame) => {
buildGameDetailsPath({ // TODO: check if user has hydra cloud
...game, // buildGameDetailsPath({
objectId: game.objectId, // ...game,
}); // objectId: game.objectId,
// });
const userParams = userProfile
? { userId: userProfile.id, displayName: userProfile.displayName }
: undefined;
return buildGameAchievementPath({ ...game }, userParams);
};
const formatPlayTime = useCallback( const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => { (playTimeInSeconds = 0) => {