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:"
},
"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:"
},
"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,
unlockTime: null,
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) {

View File

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

View File

@ -34,5 +34,20 @@ export const buildGameDetailsPath = (
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) =>
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 { useAppDispatch, useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { GameAchievement, GameShop } from "@types";
import { steamUrlBuilder } from "@shared";
import type { GameAchievement, GameShop } from "@types";
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() {
const [searchParams] = useSearchParams();
@ -11,8 +19,12 @@ export function Achievement() {
const shop = searchParams.get("shop");
const title = searchParams.get("title");
const userId = searchParams.get("userId");
const displayName = searchParams.get("displayName");
const { t } = useTranslation("achievement");
const { format } = useDate();
const navigate = useNavigate();
const dispatch = useAppDispatch();
@ -30,53 +42,109 @@ export function Achievement() {
useEffect(() => {
if (title) {
dispatch(setHeaderTitle(title + " Achievements"));
dispatch(setHeaderTitle(title));
}
}, [dispatch, title]);
return (
<div>
<h1>Achievement</h1>
if (!objectId || !shop || !title) return null;
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px`,
}}
>
{achievements.map((achievement, index) => (
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
style={{
width: "100%",
display: "flex",
flexDirection: "column",
}}
>
<h1>
{displayName
? t("user_achievements", {
displayName,
})
: t("your_achievements")}
</h1>
<div
key={index}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
justifyContent: "space-between",
marginBottom: 8,
color: vars.color.muted,
}}
title={achievement.description}
>
<img
<div
style={{
height: "60px",
width: "60px",
filter: achievement.unlocked ? "none" : "grayscale(100%)",
display: "flex",
alignItems: "center",
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={
achievement.unlocked ? achievement.icon : achievement.icongray
}
alt={achievement.displayName}
loading="lazy"
/>
<div>
<p>{achievement.displayName}</p>
<p>{achievement.description}</p>
{achievement.unlockTime && format(achievement.unlockTime)}
<small>
{achievement.unlockTime && format(achievement.unlockTime)}
</small>
</div>
</div>
</li>
))}
</div>
</ul>
</div>
);
}

View File

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

View File

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