From ac0d0efe0f7625ebfb6dbceae806b70e4de6ebc3 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:13:28 -0300 Subject: [PATCH 01/21] feat: update interfaces --- src/types/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/index.ts b/src/types/index.ts index bd9293e8..cb430c0f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,6 +41,7 @@ export interface UserAchievement { name: string; hidden: boolean; displayName: string; + points?: number; description?: string; unlocked: boolean; unlockTime: number | null; @@ -322,6 +323,7 @@ export interface TrendingGame { export interface UserStats { libraryCount: number; friendsCount: number; + achievementsPointsEarnedSum?: number; } export interface UnlockedAchievement { From 23f9b5228c7087d231a34427ea37a63f200deb41 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:40:30 -0300 Subject: [PATCH 02/21] feat: adjustments --- .../src/pages/profile/profile-content/profile-content.tsx | 1 + src/types/index.ts | 1 + 2 files changed, 2 insertions(+) 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 26975647..1792ed55 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -135,6 +135,7 @@ export function ProfileContent() { position: "relative", display: "flex", }} + title={game.title} className={styles.game} > )} {achievement.unlockTime && (
@@ -92,7 +93,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {

diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 70ce165f..f32f8161 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -14,6 +14,7 @@ import { steamUrlBuilder } from "@shared"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; import { useUserDetails } from "@renderer/hooks"; +import { useSubscription } from "@renderer/hooks/use-subscription"; const HERO_ANIMATION_THRESHOLD = 25; @@ -31,9 +32,10 @@ export function GameDetailsContent() { gameColor, setGameColor, hasNSFWContentBlocked, - handleClickOpenCheckout, } = useContext(gameDetailsContext); + const { showHydraCloudModal } = useSubscription(); + const { userDetails, hasActiveSubscription } = useUserDetails(); const { setShowCloudSyncModal, getGameArtifacts } = @@ -104,7 +106,7 @@ export function GameDetailsContent() { } if (!hasActiveSubscription) { - handleClickOpenCheckout(); + showHydraCloudModal(); return; } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 86f24f5a..95673804 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -21,6 +21,8 @@ import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { SidebarSection } from "../sidebar-section/sidebar-section"; import { buildGameAchievementPath } from "@renderer/helpers"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { useSubmit } from "react-router-dom"; +import { useSubscription } from "@renderer/hooks/use-subscription"; const fakeAchievements: UserAchievement[] = [ { @@ -67,15 +69,10 @@ export function Sidebar() { const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { - gameTitle, - shopDetails, - objectId, - shop, - stats, - achievements, - handleClickOpenCheckout, - } = useContext(gameDetailsContext); + const { gameTitle, shopDetails, objectId, shop, stats, achievements } = + useContext(gameDetailsContext); + + const { showHydraCloudModal } = useSubscription(); const { t } = useTranslation("game_details"); const { formatDateTime } = useDate(); @@ -179,7 +176,7 @@ export function Sidebar() { {!hasActiveSubscription && ( + )} +
  • -

    {t("games")}

    +

    {t("total_play_time")}

    -

    - {t("total_play_time", { - amount: formatPlayTime( - userStats.totalPlayTimeInSeconds.value - ), - })} -

    +

    {formatPlayTime(userStats.totalPlayTimeInSeconds.value)}

    {t("top_percentile", { percentile: userStats.totalPlayTimeInSeconds.topPercentile, diff --git a/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts new file mode 100644 index 00000000..a164c900 --- /dev/null +++ b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.css.ts @@ -0,0 +1,79 @@ +import { SPACING_UNIT, vars } from "../../../theme.css"; +import { style } from "@vanilla-extract/css"; + +export const friendListDisplayName = style({ + fontWeight: "bold", + fontSize: vars.size.body, + textAlign: "left", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}); + +export const friendListContainer = style({ + display: "flex", + gap: `${SPACING_UNIT * 3}px`, + alignItems: "center", + borderRadius: "4px", + border: `solid 1px ${vars.color.border}`, + width: "100%", + height: "54px", + minHeight: "54px", + transition: "all ease 0.2s", + position: "relative", + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + }, +}); + +export const friendListButton = style({ + display: "flex", + alignItems: "center", + position: "absolute", + cursor: "pointer", + height: "100%", + width: "100%", + flexDirection: "row", + color: vars.color.body, + gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`, + padding: `0 ${SPACING_UNIT}px`, +}); + +export const friendRequestItem = style({ + color: vars.color.body, + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + }, +}); + +export const acceptRequestButton = style({ + cursor: "pointer", + color: vars.color.body, + width: "28px", + height: "28px", + ":hover": { + color: vars.color.success, + }, +}); + +export const cancelRequestButton = style({ + cursor: "pointer", + color: vars.color.body, + width: "28px", + height: "28px", + ":hover": { + color: vars.color.danger, + }, +}); + +export const friendCodeButton = style({ + color: vars.color.body, + cursor: "pointer", + display: "flex", + gap: `${SPACING_UNIT / 2}px`, + alignItems: "center", + transition: "all ease 0.2s", + ":hover": { + color: vars.color.muted, + }, +}); diff --git a/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx new file mode 100644 index 00000000..1f90669f --- /dev/null +++ b/src/renderer/src/pages/shared-modals/hydra-cloud/hydra-cloud-modal.tsx @@ -0,0 +1,36 @@ +import { Button, Modal } from "@renderer/components"; +import { SPACING_UNIT } from "@renderer/theme.css"; +import { useTranslation } from "react-i18next"; + +export interface HydraCloudModalProps { + visible: boolean; + onClose: () => void; +} + +export const HydraCloudModal = ({ visible, onClose }: HydraCloudModalProps) => { + const { t } = useTranslation("hydra_cloud"); + + const handleClickOpenCheckout = () => { + window.electron.openCheckout(); + }; + + return ( + +

    + Você descobriu uma funcionalidade Hydra Cloud! + +
    + + ); +}; diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 0f2bee9f..7be495cb 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -8,6 +8,7 @@ import { toastSlice, userDetailsSlice, gameRunningSlice, + subscriptionSlice, } from "@renderer/features"; export const store = configureStore({ @@ -20,6 +21,7 @@ export const store = configureStore({ toast: toastSlice.reducer, userDetails: userDetailsSlice.reducer, gameRunning: gameRunningSlice.reducer, + subscription: subscriptionSlice.reducer, }, }); From c8566dd2beca96d34a638cf6d10313eb6c0bf485 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:56:24 -0300 Subject: [PATCH 15/21] feat: ui improvement --- src/locales/en/translation.json | 14 +-- src/locales/pt-BR/translation.json | 16 ++-- .../get-compared-unlocked-achievements.ts | 39 ++++++-- .../pages/achievements/achievement-panel.tsx | 23 +++-- .../achievements/achievements-content.tsx | 2 +- .../compared-achievement-list.tsx | 20 ++++- .../compared-achievement-panel.tsx | 2 +- .../profile/profile-content/friends-box.tsx | 4 +- .../profile-content/profile-content.css.ts | 16 ++++ .../profile-content/profile-content.tsx | 19 +++- .../profile-content/user-stats-box.tsx | 89 ++++++++++++------- src/types/index.ts | 1 + 12 files changed, 181 insertions(+), 64 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1ba8b6e2..b5576cd1 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index f716d517..7e783d5f 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -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", diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts index 0c117140..0b665212 100644 --- a/src/main/events/user/get-compared-unlocked-achievements.ts +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -13,6 +13,9 @@ const getComparedUnlockedAchievements = async ( where: { id: 1 }, }); + const showHiddenAchievementsDescription = + userPreferences?.showHiddenAchievementsDescription || false; + return HydraApi.get( `/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, diff --git a/src/renderer/src/pages/achievements/achievement-panel.tsx b/src/renderer/src/pages/achievements/achievement-panel.tsx index bf48e517..b1233bd0 100644 --- a/src/renderer/src/pages/achievements/achievement-panel.tsx +++ b/src/renderer/src/pages/achievements/achievement-panel.tsx @@ -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) { <>
    - Pontos desbloqueados: 69/420 + {t("earned_points")} + {achievementsPointsEarnedSum} / {achievementsPointsTotal}
    diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 59139768..87865280 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -329,7 +329,7 @@ export function AchievementsContent({ ) : ( <> - + )} diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx index 349ef755..44aec686 100644 --- a/src/renderer/src/pages/achievements/compared-achievement-list.tsx +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -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" />
    -

    {achievement.displayName}

    +

    + {achievement.hidden && ( + + + + )} + {achievement.displayName} +

    {achievement.description}

    diff --git a/src/renderer/src/pages/achievements/compared-achievement-panel.tsx b/src/renderer/src/pages/achievements/compared-achievement-panel.tsx index 0f35aa5d..3ac76d8d 100644 --- a/src/renderer/src/pages/achievements/compared-achievement-panel.tsx +++ b/src/renderer/src/pages/achievements/compared-achievement-panel.tsx @@ -14,7 +14,7 @@ export interface ComparedAchievementPanelProps { export function ComparedAchievementPanel({ achievements, }: ComparedAchievementPanelProps) { - const { t } = useTranslation("game_details"); + const { t } = useTranslation("achievement"); const {} = useContext(gameDetailsContext); diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index 4d3df7e8..454d13f0 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -41,9 +41,7 @@ export function FriendsBox() { {friend.displayName} {friend.currentGame && ( - -

    {t("playing", { game: friend.currentGame.title })}

    - +

    {t("playing", { game: friend.currentGame.title })}

    )}
  • diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts index b078e53d..cc462a95 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.css.ts +++ b/src/renderer/src/pages/profile/profile-content/profile-content.css.ts @@ -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", 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 f4baccd9..2217d569 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -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 && ( +
    + + {numberFormatter.format( + game.achievementsPointsEarnedSum + )} +
    + )}