feat: ui improvement

This commit is contained in:
Zamitto 2024-12-22 18:56:24 -03:00
parent 313ebc6055
commit c8566dd2be
12 changed files with 181 additions and 64 deletions

View File

@ -168,8 +168,7 @@
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected",
"available_points": "Available points:"
"no_directory_selected": "No directory selected"
},
"activation": {
"title": "Activate Hydra",
@ -367,11 +366,14 @@
"uploading_banner": "Uploading banner…",
"background_image_updated": "Background image updated",
"stats": "Stats",
"achievements": "Achievements",
"achievements": "achievements",
"games": "Games",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Ranking is updated weekly",
"playing": "Playing {{game}}"
"playing": "Playing {{game}}",
"achievements_unlocked": "Achievements Unlocked",
"earned_points": "Earned points",
"show_achievements_on_profile": "Show your achievements and earned points on your profile"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
@ -383,7 +385,9 @@
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements",
"achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}",
"hidden_achievement_tooltip": "This is a hidden achievement",
"achievement_earn_points": "Earn {{points}} points with this achievement"
"achievement_earn_points": "Earn {{points}} points with this achievement",
"earned_points": "Earned points:",
"available_points": "Available points:"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Subscription",

View File

@ -164,8 +164,7 @@
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado",
"available_points": "Pontos disponíveis:"
"no_directory_selected": "Nenhum diretório selecionado"
},
"activation": {
"title": "Ativação",
@ -365,10 +364,13 @@
"uploading_banner": "Carregando banner…",
"background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas",
"achievements": "Conquistas",
"achievements": "conquistas",
"games": "Jogos",
"ranking_updated_weekly": "Ranking é atualizado semanalmente",
"playing": "Jogando {{game}}"
"ranking_updated_weekly": "O ranking é atualizado semanalmente",
"playing": "Jogando {{game}}",
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Exiba suas conquistas e pontos no perfil"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
@ -380,7 +382,9 @@
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Está é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista"
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:"
},
"hydra_cloud": {
"subscription_tour_title": "Assinatura Hydra Cloud",

View File

@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async (
where: { id: 1 },
});
const showHiddenAchievementsDescription =
userPreferences?.showHiddenAchievementsDescription || false;
return HydraApi.get<ComparedAchievements>(
`/users/${userId}/games/achievements/compare`,
{
@ -21,15 +24,35 @@ const getComparedUnlockedAchievements = async (
language: userPreferences?.language || "en",
}
).then((achievements) => {
const sortedAchievements = achievements.achievements.sort((a, b) => {
if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1;
if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
if (a.targetStat.unlocked && b.targetStat.unlocked) {
return b.targetStat.unlockTime! - a.targetStat.unlockTime!;
}
const sortedAchievements = achievements.achievements
.sort((a, b) => {
if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1;
if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1;
if (a.targetStat.unlocked && b.targetStat.unlocked) {
return b.targetStat.unlockTime! - a.targetStat.unlockTime!;
}
return Number(a.hidden) - Number(b.hidden);
});
return Number(a.hidden) - Number(b.hidden);
})
.map((achievement) => {
if (!achievement.hidden) return achievement;
if (!achievement.ownerStat) {
return {
...achievement,
description: "",
};
}
if (!showHiddenAchievementsDescription && achievement.hidden) {
return {
...achievement,
description: "",
};
}
return achievement;
});
return {
...achievements,

View File

@ -4,13 +4,25 @@ import { gameDetailsContext } from "@renderer/context";
import * as styles from "./achievement-panel.css";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { UserAchievement } from "@types";
export interface HeroPanelProps {
isHeaderStuck: boolean;
export interface AchievementPanelProps {
achievements: UserAchievement[];
}
export function AchievementPanel({ isHeaderStuck }: HeroPanelProps) {
const { t } = useTranslation("game_details");
export function AchievementPanel({ achievements }: AchievementPanelProps) {
const { t } = useTranslation("achievement");
const achievementsPointsTotal = achievements.reduce(
(acc, achievement) => acc + (achievement.points ?? 0),
0
);
const achievementsPointsEarnedSum = achievements.reduce(
(acc, achievement) =>
acc + (achievement.unlocked ? (achievement.points ?? 0) : 0),
0
);
const {} = useContext(gameDetailsContext);
@ -18,7 +30,8 @@ export function AchievementPanel({ isHeaderStuck }: HeroPanelProps) {
<>
<div className={styles.panel}>
<div className={styles.content}>
Pontos desbloqueados: <HydraIcon width={20} height={20} /> 69/420
{t("earned_points")} <HydraIcon width={20} height={20} />
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
</div>
</div>
</>

View File

@ -329,7 +329,7 @@ export function AchievementsContent({
</>
) : (
<>
<AchievementPanel isHeaderStuck={false} />
<AchievementPanel achievements={achievements!} />
<AchievementList achievements={achievements!} />
</>
)}

View File

@ -1,8 +1,13 @@
import type { ComparedAchievements } from "@types";
import * as styles from "./achievements.css";
import { CheckCircleIcon, LockIcon } from "@primer/octicons-react";
import {
CheckCircleIcon,
EyeClosedIcon,
LockIcon,
} from "@primer/octicons-react";
import { useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useTranslation } from "react-i18next";
export interface ComparedAchievementListProps {
achievements: ComparedAchievements;
@ -11,6 +16,7 @@ export interface ComparedAchievementListProps {
export function ComparedAchievementList({
achievements,
}: ComparedAchievementListProps) {
const { t } = useTranslation("achievement");
const { formatDateTime } = useDate();
return (
@ -43,7 +49,17 @@ export function ComparedAchievementList({
loading="lazy"
/>
<div>
<h4>{achievement.displayName}</h4>
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{achievement.hidden && (
<span
style={{ display: "flex" }}
title={t("hidden_achievement_tooltip")}
>
<EyeClosedIcon size={12} />
</span>
)}
{achievement.displayName}
</h4>
<p>{achievement.description}</p>
</div>
</div>

View File

@ -14,7 +14,7 @@ export interface ComparedAchievementPanelProps {
export function ComparedAchievementPanel({
achievements,
}: ComparedAchievementPanelProps) {
const { t } = useTranslation("game_details");
const { t } = useTranslation("achievement");
const {} = useContext(gameDetailsContext);

View File

@ -41,9 +41,7 @@ export function FriendsBox() {
{friend.displayName}
</span>
{friend.currentGame && (
<Link to={buildGameDetailsPath({ ...friend.currentGame })}>
<p>{t("playing", { game: friend.currentGame.title })}</p>
</Link>
<p>{t("playing", { game: friend.currentGame.title })}</p>
)}
</div>
</Link>

View File

@ -105,6 +105,22 @@ export const listItem = style({
},
});
export const statsListItem = style({
display: "flex",
flexDirection: "column",
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}px`,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
textDecoration: "none",
},
});
export const gamesGrid = style({
listStyle: "none",
margin: "0",

View File

@ -22,6 +22,7 @@ import {
} from "@renderer/helpers";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { UserStatsBox } from "./user-stats-box";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
export function ProfileContent() {
const { userProfile, isMe, userStats } = useContext(userProfileContext);
@ -157,7 +158,7 @@ export function ProfileContent() {
height: "100%",
width: "100%",
background:
"linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%)",
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
padding: 8,
}}
>
@ -187,6 +188,22 @@ export function ProfileContent() {
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",

View File

@ -6,14 +6,13 @@ import { useFormat } from "@renderer/hooks";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
import { vars } from "@renderer/theme.css";
export function UserStatsBox() {
const { showHydraCloudModal } = useSubscription();
const { userStats } = useContext(userProfileContext);
const { userStats, isMe } = useContext(userProfileContext);
const { t } = useTranslation("user_profile");
const { numberFormatter } = useFormat();
const formatPlayTime = useCallback(
@ -43,22 +42,46 @@ export function UserStatsBox() {
<div className={styles.box}>
<ul className={styles.list}>
<li>
<h3 className={styles.listItemTitle}>{t("achievements")}</h3>
{userStats.achievementsPointsEarnedSum !== undefined ? (
<>
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
<li className={styles.statsListItem}>
<h3 className={styles.listItemTitle}>
{t("achievements_unlocked")}
</h3>
{userStats.unlockedAchievementSum !== undefined ? (
<div
style={{ display: "flex", justifyContent: "space-between" }}
>
<p
style={{
display: "flex",
alignItems: "center",
gap: "4px",
}}
>
<p className={styles.listItemDescription}>
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
{t("achievements")}
</p>
</div>
) : (
<button
type="button"
onClick={showHydraCloudModal}
className={styles.link}
>
<small style={{ color: vars.color.warning }}>
{t("show_achievements_on_profile")}
</small>
</button>
)}
</li>
)}
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
<li className={styles.statsListItem}>
<h3 className={styles.listItemTitle}>{t("earned_points")}</h3>
{userStats.achievementsPointsEarnedSum !== undefined ? (
<div
style={{ display: "flex", justifyContent: "space-between" }}
>
<p className={styles.listItemDescription}>
<HydraIcon width={20} height={20} />
{userStats.achievementsPointsEarnedSum.value}
{numberFormatter.format(
userStats.achievementsPointsEarnedSum.value
)}
</p>
<p title={t("ranking_updated_weekly")}>
{t("top_percentile", {
@ -67,25 +90,27 @@ export function UserStatsBox() {
})}
</p>
</div>
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
</>
) : (
<button
type="button"
onClick={showHydraCloudModal}
className={styles.link}
>
<small>
Saiba como exibir suas conquistas e pontos no perfil
</small>
</button>
)}
</li>
) : (
<button
type="button"
onClick={showHydraCloudModal}
className={styles.link}
>
<small style={{ color: vars.color.warning }}>
{t("show_achievements_on_profile")}
</small>
</button>
)}
</li>
)}
<li>
<li className={styles.statsListItem}>
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}</p>
<p className={styles.listItemDescription}>
<ClockIcon />
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
</p>
<p title={t("ranking_updated_weekly")}>
{t("top_percentile", {
percentile: userStats.totalPlayTimeInSeconds.topPercentile,

View File

@ -99,6 +99,7 @@ export interface UserGame {
lastTimePlayed: Date | null;
unlockedAchievementCount: number;
achievementCount: number;
achievementsPointsEarnedSum: number;
}
export interface DownloadQueue {