From 240b0705d5de8bce49a9d401fabfc8d1061fcb26 Mon Sep 17 00:00:00 2001 From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:06:19 +0300 Subject: [PATCH 01/12] Update translation.json --- src/locales/ru/translation.json | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index e576e5d9..d104a9fc 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -10,7 +10,8 @@ "hot": "Сейчас популярно", "start_typing": "Начинаю вводить текст...", "weekly": "📅 Лучшие игры недели", - "achievements": "🏆 Игры с достижениями" + "achievements": "🏆 Игры с достижениями", + "already_in_library": "Уже в библиотеке" }, "sidebar": { "catalogue": "Каталог", @@ -209,7 +210,22 @@ "invalid_wine_prefix_path": "Недопустимый путь префикса Wine", "invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.", "missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux", - "download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus." + "download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.", + "update_playtime_success": "Время игры успешно обновлено", + "update_playtime_error": "Не удалось обновить время игры", + "manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную, и это нельзя отменить.", + "artifact_renamed": "Резервная копия успешно переименована", + "rename_artifact": "Переименовать резервную копию", + "rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.", + "artifact_name_label": "Название резервной копии", + "artifact_name_placeholder": "Введите название для резервной копии", + "max_length_field": "Это поле должно содержать менее {{length}} символов", + "freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями", + "unfreeze_backup": "Открепить", + "backup_frozen": "Резервная копия закреплена", + "backup_unfrozen": "Резервная копия откреплена", + "backup_freeze_failed": "Не удалось закрепить резервную копию", + "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий" }, "activation": { "title": "Активировать Hydra", @@ -385,7 +401,8 @@ "hidden": "Скрытый", "test_notification": "Тестовое уведомление", "notification_preview": "Предварительный просмотр уведомления о достижении", - "enable_friend_start_game_notifications": "Когда друг начинает играть в игру" + "enable_friend_start_game_notifications": "Когда друг начинает играть в игру", + "enable_steam_achievements": "Включить поиск достижений Steam" }, "notifications": { "download_complete": "Загрузка завершена", @@ -403,7 +420,8 @@ "game_extracted": "{{title}} успешно распакован", "friend_started_playing_game": "{{displayName}} начал играть в игру", "test_achievement_notification_title": "Это тестовое уведомление", - "test_achievement_notification_description": "Довольно круто, да?" + "test_achievement_notification_description": "Довольно круто, да?", + "new_friend_request_description": "{{displayName}} отправил вам запрос в друзья" }, "system_tray": { "open": "Открыть Hydra", @@ -505,7 +523,9 @@ "achievements_unlocked": "Достижения разблокированы", "earned_points": "Заработано очков:", "show_achievements_on_profile": "Покажите свои достижения в профиле", - "show_points_on_profile": "Показывать заработанные очки в своем профиле" + "show_points_on_profile": "Показывать заработанные очки в своем профиле", + "error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга", + "friend_code_length_error": "Код друга должен содержать 8 символов" }, "achievement": { "achievement_unlocked": "Достижение разблокировано", From c056feb26fd872c674d9a1f19e55e19e08684b92 Mon Sep 17 00:00:00 2001 From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:24:32 +0300 Subject: [PATCH 02/12] Update translation.json --- src/locales/ru/translation.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index d104a9fc..21fb459e 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -225,7 +225,8 @@ "backup_frozen": "Резервная копия закреплена", "backup_unfrozen": "Резервная копия откреплена", "backup_freeze_failed": "Не удалось закрепить резервную копию", - "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий" + "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий", + "manual_playtime_tooltip": "Это время игры было обновлено вручную" }, "activation": { "title": "Активировать Hydra", @@ -414,8 +415,7 @@ "restart_to_install_update": "Перезапустите Hydra для установки обновления", "notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}", "notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}", - "new_friend_request_title": "Новый запрос на добавление в друзья", - "new_friend_request_description": "Вы получили новый запрос на добавление в друзья", + "new_friend_request_title": "Новый запрос на добавление в друзья", "extraction_complete": "Распаковка завершена", "game_extracted": "{{title}} успешно распакован", "friend_started_playing_game": "{{displayName}} начал играть в игру", From b22e082781b197b853a8fd45508dac276b7cdd8a Mon Sep 17 00:00:00 2001 From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:56:30 +0300 Subject: [PATCH 03/12] Update translation.json --- src/locales/ru/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 21fb459e..b1e672e1 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -415,7 +415,7 @@ "restart_to_install_update": "Перезапустите Hydra для установки обновления", "notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}", "notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}", - "new_friend_request_title": "Новый запрос на добавление в друзья", + "new_friend_request_title": "Новый запрос на добавление в друзья", "extraction_complete": "Распаковка завершена", "game_extracted": "{{title}} успешно распакован", "friend_started_playing_game": "{{displayName}} начал играть в игру", From a7e4e211673b5f0936ab8af600518fb8880ce008 Mon Sep 17 00:00:00 2001 From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com> Date: Fri, 19 Sep 2025 08:21:41 +0300 Subject: [PATCH 04/12] Update translation.json --- src/locales/ru/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index b1e672e1..b1714804 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -213,7 +213,7 @@ "download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.", "update_playtime_success": "Время игры успешно обновлено", "update_playtime_error": "Не удалось обновить время игры", - "manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную, и это нельзя отменить.", + "manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.", "artifact_renamed": "Резервная копия успешно переименована", "rename_artifact": "Переименовать резервную копию", "rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.", From cad50649aa45f781941ec004a2a2549cd4ac0d28 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 25 Sep 2025 18:07:38 +0300 Subject: [PATCH 05/12] fix: single pinned game not visible in profile --- .../profile-content/profile-content.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) 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 7b1ac8e2..68e7fecf 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -82,13 +82,14 @@ export function ProfileContent() { const hasGames = libraryGames.length > 0; const hasPinnedGames = pinnedGames.length > 0; + const hasAnyGames = hasGames || hasPinnedGames; - const shouldShowRightContent = hasGames || userProfile.friends.length > 0; + const shouldShowRightContent = hasAnyGames || userProfile.friends.length > 0; return (
- {!hasGames && ( + {!hasAnyGames && (
@@ -98,7 +99,7 @@ export function ProfileContent() {
)} - {hasGames && ( + {hasAnyGames && ( <> {hasPinnedGames && (
@@ -121,24 +122,28 @@ export function ProfileContent() {
)} -
-

{t("library")}

- {userStats && ( - {numberFormatter.format(userStats.libraryCount)} - )} -
+ {hasGames && ( +
+
+

{t("library")}

+ {userStats && ( + {numberFormatter.format(userStats.libraryCount)} + )} +
-
    - {libraryGames?.map((game) => ( - - ))} -
+
    + {libraryGames?.map((game) => ( + + ))} +
+
+ )} )}
From a29f2ba7414dfc4fe459ea5ddd4e9ee942f03016 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 25 Sep 2025 20:20:49 +0300 Subject: [PATCH 06/12] feat: added pin/unpin to the game card in profile --- .../user-profile/user-profile.context.tsx | 3 + .../profile-content/profile-content.tsx | 7 ++- .../user-library-game-card.scss | 35 ++++++++++-- .../user-library-game-card.tsx | 56 +++++++++++++++++-- 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx index 3b10b3e5..499b85ee 100644 --- a/src/renderer/src/context/user-profile/user-profile.context.tsx +++ b/src/renderer/src/context/user-profile/user-profile.context.tsx @@ -14,6 +14,7 @@ export interface UserProfileContext { isMe: boolean; userStats: UserStats | null; getUserProfile: () => Promise; + getUserLibraryGames: () => Promise; setSelectedBackgroundImage: React.Dispatch>; backgroundImage: string; badges: Badge[]; @@ -29,6 +30,7 @@ export const userProfileContext = createContext({ isMe: false, userStats: null, getUserProfile: async () => {}, + getUserLibraryGames: async () => {}, setSelectedBackgroundImage: () => {}, backgroundImage: "", badges: [], @@ -149,6 +151,7 @@ export function UserProfileContextProvider({ heroBackground, isMe, getUserProfile, + getUserLibraryGames, setSelectedBackgroundImage, backgroundImage: getBackgroundImageUrl(), userStats, 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 68e7fecf..3c27ae02 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -84,7 +84,8 @@ export function ProfileContent() { const hasPinnedGames = pinnedGames.length > 0; const hasAnyGames = hasGames || hasPinnedGames; - const shouldShowRightContent = hasAnyGames || userProfile.friends.length > 0; + const shouldShowRightContent = + hasAnyGames || userProfile.friends.length > 0; return (
@@ -127,7 +128,9 @@ export function ProfileContent() {

{t("library")}

{userStats && ( - {numberFormatter.format(userStats.libraryCount)} + + {numberFormatter.format(userStats.libraryCount)} + )}
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index ba3f5602..ab1f3456 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -65,18 +65,45 @@ padding: 8px; } - &__favorite-icon { + &__actions-container { position: absolute; top: 8px; right: 8px; - color: #ff6b6b; + display: flex; + gap: 6px; + z-index: 2; + } + + &__favorite-icon { + color: white; background-color: rgba(0, 0, 0, 0.7); border-radius: 50%; - padding: 4px; + padding: 6px; display: flex; align-items: center; justify-content: center; - z-index: 2; + } + + &__pin-button { + color: white; + background-color: rgba(0, 0, 0, 0.7); + border: none; + border-radius: 50%; + padding: 6px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.9); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } } &__playtime { diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index e64cd3bb..ca31b5de 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -1,6 +1,6 @@ import { UserGame } from "@types"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; -import { useFormat } from "@renderer/hooks"; +import { useFormat, useToast } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; import { useCallback, useContext, useState } from "react"; import { @@ -14,6 +14,8 @@ import { TrophyIcon, AlertFillIcon, HeartFillIcon, + PinIcon, + PinSlashIcon, } from "@primer/octicons-react"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; import { Tooltip } from "react-tooltip"; @@ -33,11 +35,14 @@ export function UserLibraryGameCard({ onMouseEnter, onMouseLeave, }: UserLibraryGameCardProps) { - const { userProfile } = useContext(userProfileContext); + const { userProfile, isMe, getUserLibraryGames } = useContext(userProfileContext); const { t } = useTranslation("user_profile"); + const { t: tGame } = useTranslation("game_details"); const { numberFormatter } = useFormat(); + const { showSuccessToast } = useToast(); const navigate = useNavigate(); const [isTooltipHovered, setIsTooltipHovered] = useState(false); + const [isPinning, setIsPinning] = useState(false); const getStatsItemCount = useCallback(() => { let statsCount = 1; @@ -89,6 +94,30 @@ export function UserLibraryGameCard({ [numberFormatter, t] ); + const toggleGamePinned = async () => { + setIsPinning(true); + + try { + if (game.isPinned) { + await window.electron.removeGameFromPinned(game.shop, game.objectId).then(() => { + showSuccessToast(tGame("game_removed_from_pinned")); + }); + } else { + await window.electron.addGameToPinned(game.shop, game.objectId).then(() => { + showSuccessToast(tGame("game_added_to_pinned")); + }); + } + + // Add a small delay to allow server synchronization before refreshing + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Refresh the library games to update the UI + await getUserLibraryGames(); + } finally { + setIsPinning(false); + } + }; + return ( <>
  • navigate(buildUserGameDetailsPath(game))} >
    - {game.isFavorite && ( -
    - + {(game.isFavorite || isMe) && ( +
    + {game.isFavorite && ( +
    + +
    + )} + {isMe && ( + + )}
    )} Date: Thu, 25 Sep 2025 20:23:33 +0300 Subject: [PATCH 07/12] feat: added pin/unpin to the game card in profile --- .../user-library-game-card.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index ca31b5de..8436b5d6 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -35,7 +35,8 @@ export function UserLibraryGameCard({ onMouseEnter, onMouseLeave, }: UserLibraryGameCardProps) { - const { userProfile, isMe, getUserLibraryGames } = useContext(userProfileContext); + const { userProfile, isMe, getUserLibraryGames } = + useContext(userProfileContext); const { t } = useTranslation("user_profile"); const { t: tGame } = useTranslation("game_details"); const { numberFormatter } = useFormat(); @@ -99,18 +100,22 @@ export function UserLibraryGameCard({ try { if (game.isPinned) { - await window.electron.removeGameFromPinned(game.shop, game.objectId).then(() => { - showSuccessToast(tGame("game_removed_from_pinned")); - }); + await window.electron + .removeGameFromPinned(game.shop, game.objectId) + .then(() => { + showSuccessToast(tGame("game_removed_from_pinned")); + }); } else { - await window.electron.addGameToPinned(game.shop, game.objectId).then(() => { - showSuccessToast(tGame("game_added_to_pinned")); - }); + await window.electron + .addGameToPinned(game.shop, game.objectId) + .then(() => { + showSuccessToast(tGame("game_added_to_pinned")); + }); } - + // Add a small delay to allow server synchronization before refreshing - await new Promise(resolve => setTimeout(resolve, 1000)); - + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Refresh the library games to update the UI await getUserLibraryGames(); } finally { @@ -149,7 +154,11 @@ export function UserLibraryGameCard({ }} disabled={isPinning} > - {game.isPinned ? : } + {game.isPinned ? ( + + ) : ( + + )} )}
    From fd1f13225b6ba7006a8ec765d12f80fe04a4dc97 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 25 Sep 2025 20:26:49 +0300 Subject: [PATCH 08/12] feat: added pin/unpin to the game card in profile --- .../user-library-game-card.tsx | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index 8436b5d6..e3a5911e 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -35,8 +35,7 @@ export function UserLibraryGameCard({ onMouseEnter, onMouseLeave, }: UserLibraryGameCardProps) { - const { userProfile, isMe, getUserLibraryGames } = - useContext(userProfileContext); + const { userProfile, isMe, getUserLibraryGames } = useContext(userProfileContext); const { t } = useTranslation("user_profile"); const { t: tGame } = useTranslation("game_details"); const { numberFormatter } = useFormat(); @@ -100,23 +99,17 @@ export function UserLibraryGameCard({ try { if (game.isPinned) { - await window.electron - .removeGameFromPinned(game.shop, game.objectId) - .then(() => { - showSuccessToast(tGame("game_removed_from_pinned")); - }); + await window.electron.removeGameFromPinned(game.shop, game.objectId).then(() => { + showSuccessToast(tGame("game_removed_from_pinned")); + }); } else { - await window.electron - .addGameToPinned(game.shop, game.objectId) - .then(() => { - showSuccessToast(tGame("game_added_to_pinned")); - }); + await window.electron.addGameToPinned(game.shop, game.objectId).then(() => { + showSuccessToast(tGame("game_added_to_pinned")); + }); } - - // Add a small delay to allow server synchronization before refreshing - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Refresh the library games to update the UI + + await new Promise(resolve => setTimeout(resolve, 1000)); + await getUserLibraryGames(); } finally { setIsPinning(false); @@ -154,11 +147,7 @@ export function UserLibraryGameCard({ }} disabled={isPinning} > - {game.isPinned ? ( - - ) : ( - - )} + {game.isPinned ? : } )}
    From f027f05e0214e9ab2f5b7b5f640e702d07da541b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 26 Sep 2025 16:54:10 +0300 Subject: [PATCH 09/12] feat: added functionality to collapse/expand pinned list in user profile --- .../src/hooks/use-section-collapse.ts | 30 +++ .../profile-content/profile-content.scss | 34 +++- .../profile-content/profile-content.tsx | 172 +++++++++++++++--- .../user-library-game-card.tsx | 31 ++-- 4 files changed, 229 insertions(+), 38 deletions(-) create mode 100644 src/renderer/src/hooks/use-section-collapse.ts diff --git a/src/renderer/src/hooks/use-section-collapse.ts b/src/renderer/src/hooks/use-section-collapse.ts new file mode 100644 index 00000000..3c534189 --- /dev/null +++ b/src/renderer/src/hooks/use-section-collapse.ts @@ -0,0 +1,30 @@ +import { useState, useCallback } from "react"; + +interface SectionCollapseState { + pinned: boolean; + library: boolean; +} + +export function useSectionCollapse() { + const [collapseState, setCollapseState] = useState({ + pinned: false, + library: false, + }); + + const toggleSection = useCallback( + (section: keyof SectionCollapseState) => { + setCollapseState(prevState => ({ + ...prevState, + [section]: !prevState[section], + })); + }, + [] + ); + + return { + collapseState, + toggleSection, + isPinnedCollapsed: collapseState.pinned, + isLibraryCollapsed: collapseState.library, + }; +} \ No newline at end of file diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss index 57a24e2f..4ef53a6d 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.scss +++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss @@ -54,8 +54,40 @@ &__section-header { display: flex; align-items: center; - justify-content: space-between; margin-bottom: calc(globals.$spacing-unit * 2); + gap: calc(globals.$spacing-unit); + } + + &__section-title-group { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit); + flex: 1; + } + + &__section-count { + margin-left: auto; + } + + &__collapse-button { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all ease 0.2s; + flex-shrink: 0; + + &:hover { + color: rgba(255, 255, 255, 0.9); + background-color: rgba(255, 255, 255, 0.1); + } + + } &__tabs { 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 3c27ae02..bd7eddb3 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ProfileHero } from "../profile-hero/profile-hero"; import { useAppDispatch, useFormat } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; -import { TelescopeIcon } from "@primer/octicons-react"; +import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; @@ -11,16 +11,80 @@ import { FriendsBox } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; import { UserStatsBox } from "./user-stats-box"; import { UserLibraryGameCard } from "./user-library-game-card"; +import { useSectionCollapse } from "@renderer/hooks/use-section-collapse"; +import { motion, AnimatePresence } from "framer-motion"; import "./profile-content.scss"; const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500; +const sectionVariants = { + collapsed: { + opacity: 0, + y: -20, + height: 0, + transition: { + duration: 0.3, + ease: [0.25, 0.1, 0.25, 1], + opacity: { duration: 0.1 }, + y: { duration: 0.1 }, + height: { duration: 0.2 } + } + }, + expanded: { + opacity: 1, + y: 0, + height: "auto", + transition: { + duration: 0.3, + ease: [0.25, 0.1, 0.25, 1], + opacity: { duration: 0.2, delay: 0.1 }, + y: { duration: 0.3 }, + height: { duration: 0.3 } + } + } +}; + +const gameCardVariants = { + hidden: { + opacity: 0, + y: 20, + scale: 0.95 + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + transition: { + duration: 0.4, + ease: [0.25, 0.1, 0.25, 1] + } + } +}; + +const chevronVariants = { + collapsed: { + rotate: 0, + transition: { + duration: 0.2, + ease: "easeInOut" + } + }, + expanded: { + rotate: 90, + transition: { + duration: 0.2, + ease: "easeInOut" + } + } +}; + export function ProfileContent() { const { userProfile, isMe, userStats, libraryGames, pinnedGames } = useContext(userProfileContext); const [statsIndex, setStatsIndex] = useState(0); const [isAnimationRunning, setIsAnimationRunning] = useState(true); const statsAnimation = useRef(-1); + const { toggleSection, isPinnedCollapsed } = useSectionCollapse(); const dispatch = useAppDispatch(); @@ -101,53 +165,107 @@ export function ProfileContent() { )} {hasAnyGames && ( - <> +
    {hasPinnedGames && ( -
    +
    -

    {t("pinned")}

    - {pinnedGames.length} +
    + +

    {t("pinned")}

    +
    + + {pinnedGames.length} +
    -
      - {pinnedGames?.map((game) => ( - - ))} -
    + + {!isPinnedCollapsed && ( + +
      + {pinnedGames?.map((game, index) => ( + + + + ))} +
    +
    + )} +
    )} {hasGames && (
    -

    {t("library")}

    +
    +

    {t("library")}

    +
    {userStats && ( - + {numberFormatter.format(userStats.libraryCount)} )}
      - {libraryGames?.map((game) => ( - ( + + variants={gameCardVariants} + initial="hidden" + animate="visible" + transition={{ delay: index * 0.1 }} + style={{ listStyle: 'none' }} + > + + ))}
    )} - +
    )}
    @@ -171,6 +289,8 @@ export function ProfileContent() { statsIndex, libraryGames, pinnedGames, + isPinnedCollapsed, + toggleSection, ]); return ( diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index e3a5911e..e00b5863 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -35,7 +35,8 @@ export function UserLibraryGameCard({ onMouseEnter, onMouseLeave, }: UserLibraryGameCardProps) { - const { userProfile, isMe, getUserLibraryGames } = useContext(userProfileContext); + const { userProfile, isMe, getUserLibraryGames } = + useContext(userProfileContext); const { t } = useTranslation("user_profile"); const { t: tGame } = useTranslation("game_details"); const { numberFormatter } = useFormat(); @@ -99,17 +100,21 @@ export function UserLibraryGameCard({ try { if (game.isPinned) { - await window.electron.removeGameFromPinned(game.shop, game.objectId).then(() => { - showSuccessToast(tGame("game_removed_from_pinned")); - }); + await window.electron + .removeGameFromPinned(game.shop, game.objectId) + .then(() => { + showSuccessToast(tGame("game_removed_from_pinned")); + }); } else { - await window.electron.addGameToPinned(game.shop, game.objectId).then(() => { - showSuccessToast(tGame("game_added_to_pinned")); - }); + await window.electron + .addGameToPinned(game.shop, game.objectId) + .then(() => { + showSuccessToast(tGame("game_added_to_pinned")); + }); } - - await new Promise(resolve => setTimeout(resolve, 1000)); - + + await new Promise((resolve) => setTimeout(resolve, 1000)); + await getUserLibraryGames(); } finally { setIsPinning(false); @@ -147,7 +152,11 @@ export function UserLibraryGameCard({ }} disabled={isPinning} > - {game.isPinned ? : } + {game.isPinned ? ( + + ) : ( + + )} )}
  • From b6be03cea364b30bc19fb717d8d0bcd77ecbbd9a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 26 Sep 2025 17:00:50 +0300 Subject: [PATCH 10/12] fix: formatting issues --- .../src/hooks/use-section-collapse.ts | 17 +++---- .../profile-content/profile-content.scss | 4 +- .../profile-content/profile-content.tsx | 50 +++++++++---------- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/hooks/use-section-collapse.ts b/src/renderer/src/hooks/use-section-collapse.ts index 3c534189..7cd22224 100644 --- a/src/renderer/src/hooks/use-section-collapse.ts +++ b/src/renderer/src/hooks/use-section-collapse.ts @@ -11,15 +11,12 @@ export function useSectionCollapse() { library: false, }); - const toggleSection = useCallback( - (section: keyof SectionCollapseState) => { - setCollapseState(prevState => ({ - ...prevState, - [section]: !prevState[section], - })); - }, - [] - ); + const toggleSection = useCallback((section: keyof SectionCollapseState) => { + setCollapseState((prevState) => ({ + ...prevState, + [section]: !prevState[section], + })); + }, []); return { collapseState, @@ -27,4 +24,4 @@ export function useSectionCollapse() { isPinnedCollapsed: collapseState.pinned, isLibraryCollapsed: collapseState.library, }; -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss index 4ef53a6d..8f2fcf6f 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.scss +++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss @@ -81,13 +81,11 @@ justify-content: center; transition: all ease 0.2s; flex-shrink: 0; - + &:hover { color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.1); } - - } &__tabs { 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 bd7eddb3..f1bb2ab8 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -27,8 +27,8 @@ const sectionVariants = { ease: [0.25, 0.1, 0.25, 1], opacity: { duration: 0.1 }, y: { duration: 0.1 }, - height: { duration: 0.2 } - } + height: { duration: 0.2 }, + }, }, expanded: { opacity: 1, @@ -39,16 +39,16 @@ const sectionVariants = { ease: [0.25, 0.1, 0.25, 1], opacity: { duration: 0.2, delay: 0.1 }, y: { duration: 0.3 }, - height: { duration: 0.3 } - } - } + height: { duration: 0.3 }, + }, + }, }; const gameCardVariants = { hidden: { opacity: 0, y: 20, - scale: 0.95 + scale: 0.95, }, visible: { opacity: 1, @@ -56,9 +56,9 @@ const gameCardVariants = { scale: 1, transition: { duration: 0.4, - ease: [0.25, 0.1, 0.25, 1] - } - } + ease: [0.25, 0.1, 0.25, 1], + }, + }, }; const chevronVariants = { @@ -66,16 +66,16 @@ const chevronVariants = { rotate: 0, transition: { duration: 0.2, - ease: "easeInOut" - } + ease: "easeInOut", + }, }, expanded: { rotate: 90, transition: { duration: 0.2, - ease: "easeInOut" - } - } + ease: "easeInOut", + }, + }, }; export function ProfileContent() { @@ -167,16 +167,18 @@ export function ProfileContent() { {hasAnyGames && (
    {hasPinnedGames && ( -
    +
    - + {pinnedGames.length}
    @@ -212,7 +212,7 @@ export function ProfileContent() { initial="hidden" animate="visible" transition={{ delay: index * 0.1 }} - style={{ listStyle: 'none' }} + style={{ listStyle: "none" }} > {t("library")}
    {userStats && ( - + {numberFormatter.format(userStats.libraryCount)} )} @@ -252,7 +250,7 @@ export function ProfileContent() { initial="hidden" animate="visible" transition={{ delay: index * 0.1 }} - style={{ listStyle: 'none' }} + style={{ listStyle: "none" }} > Date: Sun, 28 Sep 2025 00:37:22 +0100 Subject: [PATCH 11/12] feat: adding profile sorting --- src/locales/en/translation.json | 7 +- src/main/events/index.ts | 3 +- src/main/events/library/add-game-to-pinned.ts | 29 -- .../events/library/remove-game-from-pinned.ts | 29 -- src/main/events/library/toggle-game-pin.ts | 43 +++ src/main/events/user/get-user-library.ts | 7 +- .../library-sync/merge-with-remote-games.ts | 4 +- .../library-sync/upload-games-batch.ts | 2 +- src/preload/index.ts | 14 +- .../user-profile/user-profile.context.tsx | 37 ++- src/renderer/src/declaration.d.ts | 10 +- .../game-details/hero/hero-panel-actions.tsx | 8 +- .../modals/change-game-playtime-modal.tsx | 1 - .../profile-content/profile-content.scss | 99 ++++++- .../profile-content/profile-content.tsx | 276 ++++++++++++++---- .../user-library-game-card.tsx | 27 +- src/types/level.types.ts | 2 +- 17 files changed, 437 insertions(+), 161 deletions(-) delete mode 100644 src/main/events/library/add-game-to-pinned.ts delete mode 100644 src/main/events/library/remove-game-from-pinned.ts create mode 100644 src/main/events/library/toggle-game-pin.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b5162431..14241cdf 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -454,6 +454,9 @@ "activity": "Recent Activity", "library": "Library", "pinned": "Pinned", + "achievements_earned": "Achievements earned", + "played_recently": "Played recently", + "playtime": "Playtime", "total_play_time": "Total playtime", "manual_playtime_tooltip": "This playtime has been manually updated", "no_recent_activity_title": "Hmmm… nothing here", @@ -530,7 +533,9 @@ "show_achievements_on_profile": "Show your achievements on your profile", "show_points_on_profile": "Show your earned points on your profile", "error_adding_friend": "Could not send friend request. Please check friend code", - "friend_code_length_error": "Friend code must have 8 characters" + "friend_code_length_error": "Friend code must have 8 characters", + "game_removed_from_pinned": "Game removed from pinned", + "game_added_to_pinned": "Game added to pinned" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 00b387d2..6bd74b69 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -16,8 +16,7 @@ import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; import "./library/add-game-to-favorites"; import "./library/remove-game-from-favorites"; -import "./library/add-game-to-pinned"; -import "./library/remove-game-from-pinned"; +import "./library/toggle-game-pin"; import "./library/create-game-shortcut"; import "./library/close-game"; import "./library/delete-game-folder"; diff --git a/src/main/events/library/add-game-to-pinned.ts b/src/main/events/library/add-game-to-pinned.ts deleted file mode 100644 index 82b62d7b..00000000 --- a/src/main/events/library/add-game-to-pinned.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { registerEvent } from "../register-event"; -import { gamesSublevel, levelKeys } from "@main/level"; -import { HydraApi } from "@main/services"; -import type { GameShop } from "@types"; - -const addGameToPinned = async ( - _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string -) => { - const gameKey = levelKeys.game(shop, objectId); - - const game = await gamesSublevel.get(gameKey); - if (!game) return; - - const response = await HydraApi.put(`/profile/games/${shop}/${objectId}/pin`); - - try { - await gamesSublevel.put(gameKey, { - ...game, - pinned: true, - pinnedDate: new Date(response.pinnedDate), - }); - } catch (error) { - throw new Error(`Failed to update game pinned status: ${error}`); - } -}; - -registerEvent("addGameToPinned", addGameToPinned); diff --git a/src/main/events/library/remove-game-from-pinned.ts b/src/main/events/library/remove-game-from-pinned.ts deleted file mode 100644 index 658b9d6d..00000000 --- a/src/main/events/library/remove-game-from-pinned.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { registerEvent } from "../register-event"; -import { gamesSublevel, levelKeys } from "@main/level"; -import { HydraApi } from "@main/services"; -import type { GameShop } from "@types"; - -const removeGameFromPinned = async ( - _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string -) => { - const gameKey = levelKeys.game(shop, objectId); - - const game = await gamesSublevel.get(gameKey); - if (!game) return; - - HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`).catch(() => {}); - - try { - await gamesSublevel.put(gameKey, { - ...game, - pinned: false, - pinnedDate: null, - }); - } catch (error) { - throw new Error(`Failed to update game pinned status: ${error}`); - } -}; - -registerEvent("removeGameFromPinned", removeGameFromPinned); diff --git a/src/main/events/library/toggle-game-pin.ts b/src/main/events/library/toggle-game-pin.ts new file mode 100644 index 00000000..addedddd --- /dev/null +++ b/src/main/events/library/toggle-game-pin.ts @@ -0,0 +1,43 @@ +import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { HydraApi, logger } from "@main/services"; +import type { GameShop, UserGame } from "@types"; + +const toggleGamePin = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + pin: boolean +) => { + try { + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + if (!game) return; + + if (pin) { + const response = await HydraApi.put( + `/profile/games/${shop}/${objectId}/pin` + ); + + await gamesSublevel.put(gameKey, { + ...game, + isPinned: pin, + pinnedDate: new Date(response.pinnedDate!), + }); + } else { + await HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`); + + await gamesSublevel.put(gameKey, { + ...game, + isPinned: pin, + pinnedDate: null, + }); + } + } catch (error) { + logger.error("Failed to update game pinned status", error); + throw new Error(`Failed to update game pinned status: ${error}`); + } +}; + +registerEvent("toggleGamePin", toggleGamePin); diff --git a/src/main/events/user/get-user-library.ts b/src/main/events/user/get-user-library.ts index 8a715a49..f3c3eed5 100644 --- a/src/main/events/user/get-user-library.ts +++ b/src/main/events/user/get-user-library.ts @@ -6,13 +6,18 @@ const getUserLibrary = async ( _event: Electron.IpcMainInvokeEvent, userId: string, take: number = 12, - skip: number = 0 + skip: number = 0, + sortBy?: string ): Promise => { const params = new URLSearchParams(); params.append("take", take.toString()); params.append("skip", skip.toString()); + if (sortBy) { + params.append("sortBy", sortBy); + } + const queryString = params.toString(); const baseUrl = `/users/${userId}/library`; const url = queryString ? `${baseUrl}?${queryString}` : baseUrl; diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index 0d5d92f8..152e1138 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -37,7 +37,7 @@ export const mergeWithRemoteGames = async () => { lastTimePlayed: updatedLastTimePlayed, playTimeInMilliseconds: updatedPlayTime, favorite: game.isFavorite ?? localGame.favorite, - pinned: game.isPinned ?? localGame.pinned, + isPinned: game.isPinned ?? localGame.isPinned, }); } else { await gamesSublevel.put(gameKey, { @@ -51,7 +51,7 @@ export const mergeWithRemoteGames = async () => { hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime, isDeleted: false, favorite: game.isFavorite ?? false, - pinned: game.isPinned ?? false, + isPinned: game.isPinned ?? false, }); } diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index beab164f..d4febfea 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -27,7 +27,7 @@ export const uploadGamesBatch = async () => { shop: game.shop, lastTimePlayed: game.lastTimePlayed, isFavorite: game.favorite, - isPinned: game.pinned ?? false, + isPinned: game.isPinned ?? false, }; }) ).catch(() => {}); diff --git a/src/preload/index.ts b/src/preload/index.ts index b32fd6b0..ca275c91 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -143,10 +143,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("addGameToFavorites", shop, objectId), removeGameFromFavorites: (shop: GameShop, objectId: string) => ipcRenderer.invoke("removeGameFromFavorites", shop, objectId), - addGameToPinned: (shop: GameShop, objectId: string) => - ipcRenderer.invoke("addGameToPinned", shop, objectId), - removeGameFromPinned: (shop: GameShop, objectId: string) => - ipcRenderer.invoke("removeGameFromPinned", shop, objectId), + toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) => + ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned), updateLaunchOptions: ( shop: GameShop, objectId: string, @@ -370,8 +368,12 @@ contextBridge.exposeInMainWorld("electron", { /* User */ getUser: (userId: string) => ipcRenderer.invoke("getUser", userId), - getUserLibrary: (userId: string, take?: number, skip?: number) => - ipcRenderer.invoke("getUserLibrary", userId, take, skip), + getUserLibrary: ( + userId: string, + take?: number, + skip?: number, + sortBy?: string + ) => ipcRenderer.invoke("getUserLibrary", userId, take, skip, sortBy), blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId), unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId), getUserFriends: (userId: string, take: number, skip: number) => diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx index 499b85ee..2750442a 100644 --- a/src/renderer/src/context/user-profile/user-profile.context.tsx +++ b/src/renderer/src/context/user-profile/user-profile.context.tsx @@ -14,7 +14,7 @@ export interface UserProfileContext { isMe: boolean; userStats: UserStats | null; getUserProfile: () => Promise; - getUserLibraryGames: () => Promise; + getUserLibraryGames: (sortBy?: string) => Promise; setSelectedBackgroundImage: React.Dispatch>; backgroundImage: string; badges: Badge[]; @@ -30,7 +30,7 @@ export const userProfileContext = createContext({ isMe: false, userStats: null, getUserProfile: async () => {}, - getUserLibraryGames: async () => {}, + getUserLibraryGames: async (_sortBy?: string) => {}, setSelectedBackgroundImage: () => {}, backgroundImage: "", badges: [], @@ -93,21 +93,30 @@ export function UserProfileContextProvider({ }); }, [userId]); - const getUserLibraryGames = useCallback(async () => { - try { - const response = await window.electron.getUserLibrary(userId); - if (response) { - setLibraryGames(response.library); - setPinnedGames(response.pinnedGames); - } else { + const getUserLibraryGames = useCallback( + async (sortBy?: string) => { + try { + const response = await window.electron.getUserLibrary( + userId, + 12, + 0, + sortBy + ); + + if (response) { + setLibraryGames(response.library); + setPinnedGames(response.pinnedGames); + } else { + setLibraryGames([]); + setPinnedGames([]); + } + } catch (error) { setLibraryGames([]); setPinnedGames([]); } - } catch (error) { - setLibraryGames([]); - setPinnedGames([]); - } - }, [userId]); + }, + [userId] + ); const getUserProfile = useCallback(async () => { getUserStats(); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 115841d4..87b2d63d 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -127,8 +127,11 @@ declare global { shop: GameShop, objectId: string ) => Promise; - addGameToPinned: (shop: GameShop, objectId: string) => Promise; - removeGameFromPinned: (shop: GameShop, objectId: string) => Promise; + toggleGamePin: ( + shop: GameShop, + objectId: string, + pinned: boolean + ) => Promise; updateLaunchOptions: ( shop: GameShop, objectId: string, @@ -293,7 +296,8 @@ declare global { getUserLibrary: ( userId: string, take?: number, - skip?: number + skip?: number, + sortBy?: string ) => Promise; blockUser: (userId: string) => Promise; unblockUser: (userId: string) => Promise; diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index bdc8cf83..307de108 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -94,14 +94,14 @@ export function HeroPanelActions() { setToggleLibraryGameDisabled(true); try { - if (game?.pinned && objectId) { - await window.electron.removeGameFromPinned(shop, objectId).then(() => { + if (game?.isPinned && objectId) { + await window.electron.toggleGamePin(shop, objectId, false).then(() => { showSuccessToast(t("game_removed_from_pinned")); }); } else { if (!objectId) return; - await window.electron.addGameToPinned(shop, objectId).then(() => { + await window.electron.toggleGamePin(shop, objectId, true).then(() => { showSuccessToast(t("game_added_to_pinned")); }); } @@ -236,7 +236,7 @@ export function HeroPanelActions() { disabled={deleting} className="hero-panel-actions__action" > - {game.pinned ? : } + {game.isPinned ? : } )} diff --git a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx index c9d26b94..7355461a 100644 --- a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx @@ -72,7 +72,6 @@ export function ChangeGamePlaytimeModal({ onSuccess?.(t("update_playtime_success")); onClose(); } catch (error) { - console.log(error); onError?.(t("update_playtime_error")); } finally { setIsSubmitting(false); diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss index 8f2fcf6f..7faae2db 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.scss +++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss @@ -65,8 +65,103 @@ flex: 1; } - &__section-count { - margin-left: auto; + &__section-badge { + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + min-width: 24px; + text-align: center; + flex-shrink: 0; + } + + &__sort-container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: calc(globals.$spacing-unit); + margin-bottom: calc(globals.$spacing-unit * 2); + } + + &__sort-label { + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + font-weight: 400; + } + + &__sort-options { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit); + font-size: 14px; + } + + &__sort-option { + background: none; + border: none; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + padding: 4px 0; + font-size: 14px; + font-weight: 300; + transition: all ease 0.2s; + display: flex; + align-items: center; + gap: 6px; + + &:hover:not(:disabled) { + color: rgba(255, 255, 255, 0.6); + } + + &.active { + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + } + + &.loading { + color: rgba(201, 170, 113, 0.8); + font-weight: 500; + position: relative; + + &::after { + content: ""; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 12px; + border: 2px solid rgba(201, 170, 113, 0.3); + border-top: 2px solid rgba(201, 170, 113, 0.8); + border-radius: 50%; + animation: spin 1s linear infinite; + } + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + span { + display: inline-block; + } + } + + @keyframes spin { + 0% { + transform: translateY(-50%) rotate(0deg); + } + 100% { + transform: translateY(-50%) rotate(360deg); + } + } + + &__sort-separator { + color: rgba(255, 255, 255, 0.3); + font-size: 14px; } &__collapse-button { 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 f1bb2ab8..f330bf97 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -3,8 +3,15 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ProfileHero } from "../profile-hero/profile-hero"; import { useAppDispatch, useFormat } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; -import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react"; +import { + TelescopeIcon, + ChevronRightIcon, + TrophyIcon, + ClockIcon, + HistoryIcon, +} from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; +import { UserGame } from "@types"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; @@ -59,6 +66,35 @@ const gameCardVariants = { ease: [0.25, 0.1, 0.25, 1], }, }, + exit: { + opacity: 0, + y: -20, + scale: 0.95, + transition: { + duration: 0.3, + ease: [0.25, 0.1, 0.25, 1], + }, + }, +}; + +const gameGridVariants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + duration: 0.3, + staggerChildren: 0.1, + delayChildren: 0.1, + }, + }, + exit: { + opacity: 0, + transition: { + duration: 0.2, + }, + }, }; const chevronVariants = { @@ -78,11 +114,23 @@ const chevronVariants = { }, }; +type SortOption = "playtime" | "achievementCount" | "playedRecently"; + export function ProfileContent() { - const { userProfile, isMe, userStats, libraryGames, pinnedGames } = - useContext(userProfileContext); + const { + userProfile, + isMe, + userStats, + libraryGames, + pinnedGames, + getUserLibraryGames, + } = useContext(userProfileContext); const [statsIndex, setStatsIndex] = useState(0); const [isAnimationRunning, setIsAnimationRunning] = useState(true); + const [sortBy, setSortBy] = useState("playedRecently"); + const [isLoadingSort, setIsLoadingSort] = useState(false); + const [prevLibraryGames, setPrevLibraryGames] = useState([]); + const [prevPinnedGames, setPrevPinnedGames] = useState([]); const statsAnimation = useRef(-1); const { toggleSection, isPinnedCollapsed } = useSectionCollapse(); @@ -98,6 +146,15 @@ export function ProfileContent() { } }, [userProfile, dispatch]); + useEffect(() => { + if (userProfile) { + setIsLoadingSort(true); + getUserLibraryGames(sortBy).finally(() => { + setIsLoadingSort(false); + }); + } + }, [sortBy, getUserLibraryGames, userProfile]); + const handleOnMouseEnterGameCard = () => { setIsAnimationRunning(false); }; @@ -129,10 +186,68 @@ export function ProfileContent() { const { numberFormatter } = useFormat(); + // Function to check if game lists have changed + const gamesHaveChanged = ( + current: UserGame[], + previous: UserGame[] + ): boolean => { + if (current.length !== previous.length) return true; + return current.some( + (game, index) => game.objectId !== previous[index]?.objectId + ); + }; + + // Check if animations should run + const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames); + const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames); + + // Update previous games when lists change + useEffect(() => { + setPrevLibraryGames(libraryGames); + }, [libraryGames]); + + useEffect(() => { + setPrevPinnedGames(pinnedGames); + }, [pinnedGames]); + const usersAreFriends = useMemo(() => { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); + const SortOptions = () => ( +
    + Sort by: +
    + + | + + | + +
    +
    + ); + const content = useMemo(() => { if (!userProfile) return null; @@ -154,6 +269,8 @@ export function ProfileContent() { return (
    + {hasAnyGames && } + {!hasAnyGames && (
    @@ -188,10 +305,10 @@ export function ProfileContent() {

    {t("pinned")}

    + + {pinnedGames.length} +
    - - {pinnedGames.length} -
    @@ -204,25 +321,57 @@ export function ProfileContent() { exit="collapsed" layout > -
      - {pinnedGames?.map((game, index) => ( - - - - ))} -
    + + {shouldAnimatePinned ? ( + + {pinnedGames?.map((game, index) => ( + + + + ))} + + ) : ( + pinnedGames?.map((game) => ( +
  • + +
  • + )) + )} +
    )}
    @@ -234,33 +383,62 @@ export function ProfileContent() {

    {t("library")}

    + {userStats && ( + + {numberFormatter.format(userStats.libraryCount)} + + )}
    - {userStats && ( - - {numberFormatter.format(userStats.libraryCount)} - - )}
    -
      - {libraryGames?.map((game, index) => ( - - - - ))} -
    + + {shouldAnimateLibrary ? ( + + {libraryGames?.map((game, index) => ( + + + + ))} + + ) : ( + libraryGames?.map((game) => ( +
  • + +
  • + )) + )} +
    )}
    diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index e00b5863..860c6758 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -38,7 +38,6 @@ export function UserLibraryGameCard({ const { userProfile, isMe, getUserLibraryGames } = useContext(userProfileContext); const { t } = useTranslation("user_profile"); - const { t: tGame } = useTranslation("game_details"); const { numberFormatter } = useFormat(); const { showSuccessToast } = useToast(); const navigate = useNavigate(); @@ -99,23 +98,19 @@ export function UserLibraryGameCard({ setIsPinning(true); try { - if (game.isPinned) { - await window.electron - .removeGameFromPinned(game.shop, game.objectId) - .then(() => { - showSuccessToast(tGame("game_removed_from_pinned")); - }); - } else { - await window.electron - .addGameToPinned(game.shop, game.objectId) - .then(() => { - showSuccessToast(tGame("game_added_to_pinned")); - }); - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); + await window.electron.toggleGamePin( + game.shop, + game.objectId, + !game.isPinned + ); await getUserLibraryGames(); + + if (game.isPinned) { + showSuccessToast(t("game_removed_from_pinned")); + } else { + showSuccessToast(t("game_added_to_pinned")); + } } finally { setIsPinning(false); } diff --git a/src/types/level.types.ts b/src/types/level.types.ts index da702b70..c5bd3454 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -44,7 +44,7 @@ export interface Game { executablePath?: string | null; launchOptions?: string | null; favorite?: boolean; - pinned?: boolean; + isPinned?: boolean; pinnedDate?: Date | null; automaticCloudSync?: boolean; hasManuallyUpdatedPlaytime?: boolean; From 2cebc73789bcc656f2d3e429d010d40afc992b9e Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 28 Sep 2025 00:50:51 +0100 Subject: [PATCH 12/12] feat: adding profile sorting --- .../profile-content/profile-content.scss | 86 ---------- .../profile-content/profile-content.tsx | 158 ++---------------- 2 files changed, 16 insertions(+), 228 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss index 7faae2db..2f274e11 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.scss +++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss @@ -77,92 +77,6 @@ flex-shrink: 0; } - &__sort-container { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: calc(globals.$spacing-unit); - margin-bottom: calc(globals.$spacing-unit * 2); - } - - &__sort-label { - color: rgba(255, 255, 255, 0.6); - font-size: 14px; - font-weight: 400; - } - - &__sort-options { - display: flex; - align-items: center; - gap: calc(globals.$spacing-unit); - font-size: 14px; - } - - &__sort-option { - background: none; - border: none; - color: rgba(255, 255, 255, 0.4); - cursor: pointer; - padding: 4px 0; - font-size: 14px; - font-weight: 300; - transition: all ease 0.2s; - display: flex; - align-items: center; - gap: 6px; - - &:hover:not(:disabled) { - color: rgba(255, 255, 255, 0.6); - } - - &.active { - color: rgba(255, 255, 255, 0.9); - font-weight: 500; - } - - &.loading { - color: rgba(201, 170, 113, 0.8); - font-weight: 500; - position: relative; - - &::after { - content: ""; - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); - width: 12px; - height: 12px; - border: 2px solid rgba(201, 170, 113, 0.3); - border-top: 2px solid rgba(201, 170, 113, 0.8); - border-radius: 50%; - animation: spin 1s linear infinite; - } - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - - span { - display: inline-block; - } - } - - @keyframes spin { - 0% { - transform: translateY(-50%) rotate(0deg); - } - 100% { - transform: translateY(-50%) rotate(360deg); - } - } - - &__sort-separator { - color: rgba(255, 255, 255, 0.3); - font-size: 14px; - } &__collapse-button { background: none; 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 f330bf97..8de16d3d 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -3,13 +3,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ProfileHero } from "../profile-hero/profile-hero"; import { useAppDispatch, useFormat } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; -import { - TelescopeIcon, - ChevronRightIcon, - TrophyIcon, - ClockIcon, - HistoryIcon, -} from "@primer/octicons-react"; +import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import { UserGame } from "@types"; import { LockedProfile } from "./locked-profile"; @@ -18,102 +12,18 @@ import { FriendsBox } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; import { UserStatsBox } from "./user-stats-box"; import { UserLibraryGameCard } from "./user-library-game-card"; +import { SortOptions } from "./sort-options"; import { useSectionCollapse } from "@renderer/hooks/use-section-collapse"; import { motion, AnimatePresence } from "framer-motion"; +import { + sectionVariants, + gameCardVariants, + gameGridVariants, + chevronVariants, + GAME_STATS_ANIMATION_DURATION_IN_MS, +} from "./profile-animations"; import "./profile-content.scss"; -const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500; - -const sectionVariants = { - collapsed: { - opacity: 0, - y: -20, - height: 0, - transition: { - duration: 0.3, - ease: [0.25, 0.1, 0.25, 1], - opacity: { duration: 0.1 }, - y: { duration: 0.1 }, - height: { duration: 0.2 }, - }, - }, - expanded: { - opacity: 1, - y: 0, - height: "auto", - transition: { - duration: 0.3, - ease: [0.25, 0.1, 0.25, 1], - opacity: { duration: 0.2, delay: 0.1 }, - y: { duration: 0.3 }, - height: { duration: 0.3 }, - }, - }, -}; - -const gameCardVariants = { - hidden: { - opacity: 0, - y: 20, - scale: 0.95, - }, - visible: { - opacity: 1, - y: 0, - scale: 1, - transition: { - duration: 0.4, - ease: [0.25, 0.1, 0.25, 1], - }, - }, - exit: { - opacity: 0, - y: -20, - scale: 0.95, - transition: { - duration: 0.3, - ease: [0.25, 0.1, 0.25, 1], - }, - }, -}; - -const gameGridVariants = { - hidden: { - opacity: 0, - }, - visible: { - opacity: 1, - transition: { - duration: 0.3, - staggerChildren: 0.1, - delayChildren: 0.1, - }, - }, - exit: { - opacity: 0, - transition: { - duration: 0.2, - }, - }, -}; - -const chevronVariants = { - collapsed: { - rotate: 0, - transition: { - duration: 0.2, - ease: "easeInOut", - }, - }, - expanded: { - rotate: 90, - transition: { - duration: 0.2, - ease: "easeInOut", - }, - }, -}; - type SortOption = "playtime" | "achievementCount" | "playedRecently"; export function ProfileContent() { @@ -128,7 +38,6 @@ export function ProfileContent() { const [statsIndex, setStatsIndex] = useState(0); const [isAnimationRunning, setIsAnimationRunning] = useState(true); const [sortBy, setSortBy] = useState("playedRecently"); - const [isLoadingSort, setIsLoadingSort] = useState(false); const [prevLibraryGames, setPrevLibraryGames] = useState([]); const [prevPinnedGames, setPrevPinnedGames] = useState([]); const statsAnimation = useRef(-1); @@ -148,10 +57,7 @@ export function ProfileContent() { useEffect(() => { if (userProfile) { - setIsLoadingSort(true); - getUserLibraryGames(sortBy).finally(() => { - setIsLoadingSort(false); - }); + getUserLibraryGames(sortBy); } }, [sortBy, getUserLibraryGames, userProfile]); @@ -186,7 +92,6 @@ export function ProfileContent() { const { numberFormatter } = useFormat(); - // Function to check if game lists have changed const gamesHaveChanged = ( current: UserGame[], previous: UserGame[] @@ -197,11 +102,9 @@ export function ProfileContent() { ); }; - // Check if animations should run const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames); const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames); - // Update previous games when lists change useEffect(() => { setPrevLibraryGames(libraryGames); }, [libraryGames]); @@ -214,40 +117,6 @@ export function ProfileContent() { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); - const SortOptions = () => ( -
    - Sort by: -
    - - | - - | - -
    -
    - ); - const content = useMemo(() => { if (!userProfile) return null; @@ -269,7 +138,9 @@ export function ProfileContent() { return (
    - {hasAnyGames && } + {hasAnyGames && ( + + )} {!hasAnyGames && (
    @@ -467,6 +338,9 @@ export function ProfileContent() { pinnedGames, isPinnedCollapsed, toggleSection, + shouldAnimateLibrary, + shouldAnimatePinned, + sortBy, ]); return (