diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b80f8bd6..6d2ea93b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index c5a15f6b..f5d91d46 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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}}" } } diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 07bc2e91..8b040874 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -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) { diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 917e5351..dc43f827 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -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"], }, ]; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index a9fc3cdd..2eb83df6 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -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(); diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts new file mode 100644 index 00000000..f5f548e6 --- /dev/null +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -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, + }, +}); diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index dd50c0ab..19152f9c 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -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 ( -
-

Achievement

+ if (!objectId || !shop || !title) return null; -
- {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 ( +
+
+ +
+

+ {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

- + + + {unlockedAchievementCount} / {totalAchievementCount} + +
+ + + {formatDownloadProgress( + unlockedAchievementCount / totalAchievementCount + )} + +
+ +
+
+ +
+ ))} -
+ ); } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index fd39deeb..e05a9187 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -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) => (
  • @@ -116,7 +111,11 @@ export function Sidebar() { {t("see_all_achievements")} 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 b7c955f9..312ea963 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -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) => {