From f027f05e0214e9ab2f5b7b5f640e702d07da541b Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 26 Sep 2025 16:54:10 +0300 Subject: [PATCH] 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 ? ( + + ) : ( + + )} )}