feat: update achievements on sidebar

This commit is contained in:
Zamitto 2024-10-08 15:17:53 -03:00
parent 446b03eeff
commit ffac677e3f
9 changed files with 143 additions and 57 deletions

View File

@ -145,7 +145,8 @@
"no_backups": "You haven't created any backups for this game yet", "no_backups": "You haven't created any backups for this game yet",
"backup_uploaded": "Backup uploaded", "backup_uploaded": "Backup uploaded",
"backup_deleted": "Backup deleted", "backup_deleted": "Backup deleted",
"backup_restored": "Backup restored" "backup_restored": "Backup restored",
"see_all_achievements": "See all achievements"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -127,7 +127,7 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"", "executable_path_in_use": "Executável em uso por \"{{game}}\"",
"warning": "Aviso:", "warning": "Aviso:",
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
"achievements": "Conquistas {{unlockedCount}}/{{achievementsCount}}", "achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"cloud_save": "Salvamento em nuvem", "cloud_save": "Salvamento em nuvem",
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo", "cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
"backups": "Backups", "backups": "Backups",
@ -141,7 +141,8 @@
"no_backups": "Você ainda não fez nenhum backup deste jogo", "no_backups": "Você ainda não fez nenhum backup deste jogo",
"backup_uploaded": "Backup criado", "backup_uploaded": "Backup criado",
"backup_deleted": "Backup apagado", "backup_deleted": "Backup apagado",
"backup_restored": "Backup restaurado" "backup_restored": "Backup restaurado",
"see_all_achievements": "Ver todas as conquistas"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -116,7 +116,8 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"", "executable_path_in_use": "Executável em uso por \"{{game}}\"",
"warning": "Aviso:", "warning": "Aviso:",
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
"achievements": "Conquistas {{unlockedCount}}/{{achievementsCount}}" "achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"see_all_achievements": "Ver todas as conquistas"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -1,4 +1,9 @@
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types"; import type {
AchievementData,
GameAchievement,
GameShop,
UnlockedAchievement,
} from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { import {
gameAchievementRepository, gameAchievementRepository,
@ -18,7 +23,7 @@ const getAchievements = async (
where: { objectId, shop }, where: { objectId, shop },
}); });
const achievementsData = cachedAchievements?.achievements const achievementsData: AchievementData[] = cachedAchievements?.achievements
? JSON.parse(cachedAchievements.achievements) ? JSON.parse(cachedAchievements.achievements)
: await getGameAchievementData(objectId, shop); : await getGameAchievementData(objectId, shop);
@ -51,7 +56,7 @@ export const getGameAchievements = async (
return achievementsData return achievementsData
.map((achievementData) => { .map((achievementData) => {
const unlockedAchiement = unlockedAchievements.find( const unlockedAchiementData = unlockedAchievements.find(
(localAchievement) => { (localAchievement) => {
return ( return (
localAchievement.name.toUpperCase() == localAchievement.name.toUpperCase() ==
@ -60,20 +65,33 @@ export const getGameAchievements = async (
} }
); );
if (unlockedAchiement) { const icongray = achievementData.icongray.endsWith("/")
? achievementData.icon
: achievementData.icongray;
if (unlockedAchiementData) {
return { return {
...achievementData, ...achievementData,
unlocked: true, unlocked: true,
unlockTime: unlockedAchiement.unlockTime, unlockTime: unlockedAchiementData.unlockTime,
icongray,
}; };
} }
return { ...achievementData, unlocked: false, unlockTime: null }; return {
...achievementData,
unlocked: false,
unlockTime: null,
icongray,
};
}) })
.sort((a, b) => { .sort((a: GameAchievement, b: GameAchievement) => {
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;
return b.unlockTime - a.unlockTime; if (a.unlocked && b.unlocked) {
return b.unlockTime! - a.unlockTime!;
}
return Number(a.hidden) - Number(b.hidden);
}); });
}; };

View File

@ -39,6 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
const title = useMemo(() => { const title = useMemo(() => {
if (location.pathname.startsWith("/game")) return headerTitle; if (location.pathname.startsWith("/game")) return headerTitle;
if (location.pathname.startsWith("/achievements")) return headerTitle;
if (location.pathname.startsWith("/profile")) return headerTitle; if (location.pathname.startsWith("/profile")) return headerTitle;
if (location.pathname.startsWith("/search")) return t("search_results"); if (location.pathname.startsWith("/search")) return t("search_results");

View File

@ -1,4 +1,5 @@
import { useDate } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features";
import { useAppDispatch, useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { GameAchievement, GameShop } from "@types"; import { GameAchievement, GameShop } from "@types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -8,10 +9,13 @@ export function Achievement() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const objectId = searchParams.get("objectId"); const objectId = searchParams.get("objectId");
const shop = searchParams.get("shop"); const shop = searchParams.get("shop");
const title = searchParams.get("title");
const userId = searchParams.get("userId"); const userId = searchParams.get("userId");
const { format } = useDate(); const { format } = useDate();
const dispatch = useAppDispatch();
const [achievements, setAchievements] = useState<GameAchievement[]>([]); const [achievements, setAchievements] = useState<GameAchievement[]>([]);
useEffect(() => { useEffect(() => {
@ -24,6 +28,12 @@ export function Achievement() {
} }
}, [objectId, shop, userId]); }, [objectId, shop, userId]);
useEffect(() => {
if (title) {
dispatch(setHeaderTitle(title + " Achievements"));
}
}, [dispatch, title]);
return ( return (
<div> <div>
<h1>Achievement</h1> <h1>Achievement</h1>
@ -61,6 +71,7 @@ export function Achievement() {
/> />
<div> <div>
<p>{achievement.displayName}</p> <p>{achievement.displayName}</p>
<p>{achievement.description}</p>
{achievement.unlockTime && format(achievement.unlockTime)} {achievement.unlockTime && format(achievement.unlockTime)}
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import { globalStyle, style } from "@vanilla-extract/css"; import { globalStyle, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../../theme.css"; import { SPACING_UNIT, vars } from "../../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
export const contentSidebar = style({ export const contentSidebar = style({
borderLeft: `solid 1px ${vars.color.border}`, borderLeft: `solid 1px ${vars.color.border}`,
@ -110,3 +111,46 @@ globalStyle(`${requirementsDetails} a`, {
display: "flex", display: "flex",
color: vars.color.body, color: vars.color.body,
}); });
export const list = style({
listStyle: "none",
margin: "0",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT * 2}px`,
});
export const listItem = style({
display: "flex",
cursor: "pointer",
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%)",
},
},
},
});

View File

@ -7,10 +7,10 @@ import * as styles from "./sidebar.css";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import { useDate, useFormat } from "@renderer/hooks"; import { useDate, useFormat } from "@renderer/hooks";
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import { SPACING_UNIT } from "@renderer/theme.css";
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 { useNavigate } from "react-router-dom";
export function Sidebar() { export function Sidebar() {
const [howLongToBeat, setHowLongToBeat] = useState<{ const [howLongToBeat, setHowLongToBeat] = useState<{
@ -30,7 +30,12 @@ export function Sidebar() {
const { numberFormatter } = useFormat(); const { numberFormatter } = useFormat();
const buildGameAchievementPath = () => { const buildGameAchievementPath = () => {
const urlParams = new URLSearchParams({ objectId: objectId!, shop }); const urlParams = new URLSearchParams({
objectId: objectId!,
shop,
title: gameTitle,
});
return `/achievements?${urlParams.toString()}`; return `/achievements?${urlParams.toString()}`;
}; };
@ -80,49 +85,43 @@ export function Sidebar() {
achievementsCount: achievements.length, achievementsCount: achievements.length,
})} })}
> >
<span> <ul className={styles.list}>
<Link to={buildGameAchievementPath()}>Ver todas</Link> {achievements.slice(0, 4).map((achievement, index) => (
</span> <li key={index}>
<div <Link
style={{ to={buildGameAchievementPath()}
display: "flex", className={styles.listItem}
flexDirection: "column", title={achievement.description}
gap: `${SPACING_UNIT}px`, >
padding: `${SPACING_UNIT * 2}px`, <img
}} className={styles.listItemImage({
> unlocked: achievement.unlocked,
{achievements.slice(0, 6).map((achievement, index) => ( })}
<div src={
key={index} achievement.unlocked
style={{ ? achievement.icon
display: "flex", : achievement.icongray
flexDirection: "row", }
alignItems: "center", alt={achievement.displayName}
gap: `${SPACING_UNIT}px`, loading="lazy"
}} />
title={achievement.description} <div>
> <p>{achievement.displayName}</p>
<img <small>
style={{ {achievement.unlockTime && format(achievement.unlockTime)}
height: "60px", </small>
width: "60px", </div>
filter: achievement.unlocked ? "none" : "grayscale(100%)", </Link>
}} </li>
src={
achievement.unlocked
? achievement.icon
: achievement.icongray
}
alt={achievement.displayName}
loading="lazy"
/>
<div>
<p>{achievement.displayName}</p>
{achievement.unlockTime && format(achievement.unlockTime)}
</div>
</div>
))} ))}
</div>
<Link
style={{ textAlign: "center" }}
to={buildGameAchievementPath()}
>
{t("see_all_achievements")}
</Link>
</ul>
</SidebarSection> </SidebarSection>
)} )}

View File

@ -28,6 +28,15 @@ export interface GameRepack {
updatedAt: Date; updatedAt: Date;
} }
export interface AchievementData {
name: string;
displayName: string;
description?: string;
icon: string;
icongray: string;
hidden: boolean;
}
export interface GameAchievement { export interface GameAchievement {
name: string; name: string;
displayName: string; displayName: string;
@ -36,6 +45,7 @@ export interface GameAchievement {
unlockTime: number | null; unlockTime: number | null;
icon: string; icon: string;
icongray: string; icongray: string;
hidden: boolean;
} }
export type ShopDetails = SteamAppDetails & { export type ShopDetails = SteamAppDetails & {