mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 04:41:03 +00:00
feat: adding profile sorting
This commit is contained in:
@@ -77,92 +77,6 @@
|
|||||||
flex-shrink: 0;
|
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 {
|
&__collapse-button {
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import {
|
import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react";
|
||||||
TelescopeIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
TrophyIcon,
|
|
||||||
ClockIcon,
|
|
||||||
HistoryIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
@@ -18,102 +12,18 @@ import { FriendsBox } from "./friends-box";
|
|||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserStatsBox } from "./user-stats-box";
|
import { UserStatsBox } from "./user-stats-box";
|
||||||
import { UserLibraryGameCard } from "./user-library-game-card";
|
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||||
|
import { SortOptions } from "./sort-options";
|
||||||
import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
|
import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
sectionVariants,
|
||||||
|
gameCardVariants,
|
||||||
|
gameGridVariants,
|
||||||
|
chevronVariants,
|
||||||
|
GAME_STATS_ANIMATION_DURATION_IN_MS,
|
||||||
|
} from "./profile-animations";
|
||||||
import "./profile-content.scss";
|
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";
|
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
@@ -128,7 +38,6 @@ export function ProfileContent() {
|
|||||||
const [statsIndex, setStatsIndex] = useState(0);
|
const [statsIndex, setStatsIndex] = useState(0);
|
||||||
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||||
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
||||||
const [isLoadingSort, setIsLoadingSort] = useState(false);
|
|
||||||
const [prevLibraryGames, setPrevLibraryGames] = useState<UserGame[]>([]);
|
const [prevLibraryGames, setPrevLibraryGames] = useState<UserGame[]>([]);
|
||||||
const [prevPinnedGames, setPrevPinnedGames] = useState<UserGame[]>([]);
|
const [prevPinnedGames, setPrevPinnedGames] = useState<UserGame[]>([]);
|
||||||
const statsAnimation = useRef(-1);
|
const statsAnimation = useRef(-1);
|
||||||
@@ -148,10 +57,7 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
setIsLoadingSort(true);
|
getUserLibraryGames(sortBy);
|
||||||
getUserLibraryGames(sortBy).finally(() => {
|
|
||||||
setIsLoadingSort(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [sortBy, getUserLibraryGames, userProfile]);
|
}, [sortBy, getUserLibraryGames, userProfile]);
|
||||||
|
|
||||||
@@ -186,7 +92,6 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
// Function to check if game lists have changed
|
|
||||||
const gamesHaveChanged = (
|
const gamesHaveChanged = (
|
||||||
current: UserGame[],
|
current: UserGame[],
|
||||||
previous: UserGame[]
|
previous: UserGame[]
|
||||||
@@ -197,11 +102,9 @@ export function ProfileContent() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if animations should run
|
|
||||||
const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames);
|
const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames);
|
||||||
const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames);
|
const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames);
|
||||||
|
|
||||||
// Update previous games when lists change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPrevLibraryGames(libraryGames);
|
setPrevLibraryGames(libraryGames);
|
||||||
}, [libraryGames]);
|
}, [libraryGames]);
|
||||||
@@ -214,40 +117,6 @@ export function ProfileContent() {
|
|||||||
return userProfile?.relation?.status === "ACCEPTED";
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const SortOptions = () => (
|
|
||||||
<div className="profile-content__sort-container">
|
|
||||||
<span className="profile-content__sort-label">Sort by:</span>
|
|
||||||
<div className="profile-content__sort-options">
|
|
||||||
<button
|
|
||||||
className={`profile-content__sort-option ${sortBy === "achievementCount" ? "active" : ""} ${isLoadingSort && sortBy === "achievementCount" ? "loading" : ""}`}
|
|
||||||
onClick={() => setSortBy("achievementCount")}
|
|
||||||
disabled={isLoadingSort}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={16} />
|
|
||||||
<span>{t("achievements_earned")}</span>
|
|
||||||
</button>
|
|
||||||
<span className="profile-content__sort-separator">|</span>
|
|
||||||
<button
|
|
||||||
className={`profile-content__sort-option ${sortBy === "playedRecently" ? "active" : ""} ${isLoadingSort && sortBy === "playedRecently" ? "loading" : ""}`}
|
|
||||||
onClick={() => setSortBy("playedRecently")}
|
|
||||||
disabled={isLoadingSort}
|
|
||||||
>
|
|
||||||
<HistoryIcon size={16} />
|
|
||||||
<span>{t("played_recently")}</span>
|
|
||||||
</button>
|
|
||||||
<span className="profile-content__sort-separator">|</span>
|
|
||||||
<button
|
|
||||||
className={`profile-content__sort-option ${sortBy === "playtime" ? "active" : ""} ${isLoadingSort && sortBy === "playtime" ? "loading" : ""}`}
|
|
||||||
onClick={() => setSortBy("playtime")}
|
|
||||||
disabled={isLoadingSort}
|
|
||||||
>
|
|
||||||
<ClockIcon size={16} />
|
|
||||||
<span>{t("playtime")}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
||||||
@@ -269,7 +138,9 @@ export function ProfileContent() {
|
|||||||
return (
|
return (
|
||||||
<section className="profile-content__section">
|
<section className="profile-content__section">
|
||||||
<div className="profile-content__main">
|
<div className="profile-content__main">
|
||||||
{hasAnyGames && <SortOptions />}
|
{hasAnyGames && (
|
||||||
|
<SortOptions sortBy={sortBy} onSortChange={setSortBy} />
|
||||||
|
)}
|
||||||
|
|
||||||
{!hasAnyGames && (
|
{!hasAnyGames && (
|
||||||
<div className="profile-content__no-games">
|
<div className="profile-content__no-games">
|
||||||
@@ -467,6 +338,9 @@ export function ProfileContent() {
|
|||||||
pinnedGames,
|
pinnedGames,
|
||||||
isPinnedCollapsed,
|
isPinnedCollapsed,
|
||||||
toggleSection,
|
toggleSection,
|
||||||
|
shouldAnimateLibrary,
|
||||||
|
shouldAnimatePinned,
|
||||||
|
sortBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user