mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
feat: redoing page
This commit is contained in:
parent
2700f27d03
commit
ab27fd21d7
@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import {
|
||||
setProfileBackground,
|
||||
@ -129,7 +129,16 @@ export function useUserDetails() {
|
||||
|
||||
const unblockUser = (userId: string) => window.electron.unblockUser(userId);
|
||||
|
||||
const hasActiveSubscription = userDetails?.subscription?.status === "active";
|
||||
const hasActiveSubscription = useMemo(() => {
|
||||
if (!userDetails?.subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
userDetails.subscription.expiresAt == null ||
|
||||
new Date(userDetails.subscription.expiresAt) > new Date()
|
||||
);
|
||||
}, [userDetails]);
|
||||
|
||||
return {
|
||||
userDetails,
|
||||
|
@ -5,15 +5,19 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievements.css";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
LockIcon,
|
||||
PersonIcon,
|
||||
TrophyIcon,
|
||||
UnlockIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { UserAchievement } from "@types";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
@ -32,180 +36,85 @@ interface AchievementListProps {
|
||||
|
||||
interface AchievementPanelProps {
|
||||
user: UserInfo;
|
||||
otherUser: UserInfo | null;
|
||||
}
|
||||
|
||||
function AchievementPanel({ user, otherUser }: AchievementPanelProps) {
|
||||
const { t } = useTranslation("achievement");
|
||||
const { userDetails } = useUserDetails();
|
||||
function AchievementPanel({ user }: AchievementPanelProps) {
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
|
||||
const userTotalAchievementCount = user.achievements.length;
|
||||
const userUnlockedAchievementCount = user.achievements.filter(
|
||||
(achievement) => achievement.unlocked
|
||||
).length;
|
||||
|
||||
if (!otherUser) {
|
||||
const getProfileImage = (user: UserInfo) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||
{t("your_achievements")}
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
width: "100%",
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{userUnlockedAchievementCount} / {userTotalAchievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
userUnlockedAchievementCount / userTotalAchievementCount
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
max={1}
|
||||
value={userUnlockedAchievementCount / userTotalAchievementCount}
|
||||
className={styles.achievementsProgressBar}
|
||||
<div className={styles.profileAvatar}>
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const otherUserUnlockedAchievementCount = otherUser.achievements.filter(
|
||||
(achievement) => achievement.unlocked
|
||||
).length;
|
||||
const otherUserTotalAchievementCount = otherUser.achievements.length;
|
||||
if (userDetails?.id == user.userId && !hasActiveSubscription) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "2fr 1fr 1fr",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{getProfileImage(user)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||
{otherUser.displayName}
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
color: vars.color.muted,
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{otherUserUnlockedAchievementCount} /{" "}
|
||||
{otherUserTotalAchievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
otherUserUnlockedAchievementCount /
|
||||
otherUserTotalAchievementCount
|
||||
)}
|
||||
{userUnlockedAchievementCount} / {userTotalAchievementCount}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
otherUserUnlockedAchievementCount / otherUserTotalAchievementCount
|
||||
}
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||
{userDetails?.displayName}
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
width: "100%",
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{userUnlockedAchievementCount} / {userTotalAchievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
userUnlockedAchievementCount / userTotalAchievementCount
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
max={1}
|
||||
value={userUnlockedAchievementCount / userTotalAchievementCount}
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
userUnlockedAchievementCount / userTotalAchievementCount
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
max={1}
|
||||
value={userUnlockedAchievementCount / userTotalAchievementCount}
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -220,18 +129,6 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const getProfileImage = (imageUrl: string | null | undefined) => {
|
||||
return (
|
||||
<div className={styles.profileAvatar}>
|
||||
{imageUrl ? (
|
||||
<img className={styles.profileAvatar} src={imageUrl} alt={"teste"} />
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!otherUserAchievements || otherUserAchievements.length === 0) {
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
@ -271,7 +168,7 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
|
||||
<li
|
||||
key={index}
|
||||
className={styles.listItem}
|
||||
style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr" }}
|
||||
style={{ display: "grid", gridTemplateColumns: "3fr 1fr 1fr" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@ -295,32 +192,46 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
title={
|
||||
otherUserAchievement.unlockTime
|
||||
? formatDateTime(otherUserAchievement.unlockTime)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{otherUserAchievement.unlockTime ? (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
{getProfileImage(otherUser.profileImageUrl)}
|
||||
<small>{t("unlocked_at")}</small>
|
||||
<p>{formatDateTime(otherUserAchievement.unlockTime)}</p>
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<small>{formatDateTime(otherUserAchievement.unlockTime)}</small>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<LockIcon />
|
||||
<p>Não desbloqueada</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
title={
|
||||
userDetails?.subscription && achievements[index].unlockTime
|
||||
? formatDateTime(achievements[index].unlockTime)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{userDetails?.subscription && achievements[index].unlockTime ? (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
{getProfileImage(user.profileImageUrl)}
|
||||
<small>{t("unlocked_at")}</small>
|
||||
<UnlockIcon />
|
||||
<p>{formatDateTime(achievements[index].unlockTime)}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<LockIcon />
|
||||
<p>Não desbloqueada</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -334,7 +245,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
const heroRef = useRef<HTMLDivElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||
|
||||
const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } =
|
||||
useContext(gameDetailsContext);
|
||||
@ -380,11 +290,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
|
||||
|
||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||
const opacity = Math.max(
|
||||
0,
|
||||
1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD)
|
||||
);
|
||||
|
||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
||||
setIsHeaderStuck(true);
|
||||
}
|
||||
@ -392,8 +297,22 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
if (scrollY <= heroHeight && isHeaderStuck) {
|
||||
setIsHeaderStuck(false);
|
||||
}
|
||||
};
|
||||
|
||||
setBackdropOpacity(opacity);
|
||||
const getProfileImage = (user: UserInfo) => {
|
||||
return (
|
||||
<div className={styles.profileAvatarSmall}>
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatarSmall}
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={24} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
||||
@ -402,6 +321,7 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
<div className={styles.wrapper}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectId)}
|
||||
style={{ display: "none" }}
|
||||
alt={gameTitle}
|
||||
className={styles.heroImage}
|
||||
onLoad={handleHeroLoad}
|
||||
@ -412,19 +332,14 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
onScroll={onScroll}
|
||||
className={styles.container}
|
||||
>
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: gameColor,
|
||||
flex: 1,
|
||||
opacity: Math.min(1, 1 - backdropOpactiy),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.heroLogoBackdrop}
|
||||
style={{ opacity: backdropOpactiy }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
||||
}}
|
||||
>
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectId)}
|
||||
@ -433,18 +348,50 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<AchievementPanel
|
||||
user={{
|
||||
...userDetails,
|
||||
userId: userDetails.id,
|
||||
achievements: sortedAchievements,
|
||||
}}
|
||||
/>
|
||||
|
||||
{otherUser && <AchievementPanel user={otherUser} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.panel({ stuck: isHeaderStuck })}>
|
||||
<AchievementPanel
|
||||
user={{
|
||||
...userDetails,
|
||||
userId: userDetails.id,
|
||||
achievements: sortedAchievements,
|
||||
}}
|
||||
otherUser={otherUser}
|
||||
/>
|
||||
</div>
|
||||
{otherUser && (
|
||||
<div className={styles.panel({ stuck: isHeaderStuck })}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "3fr 1fr 1fr",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
<div>{getProfileImage(otherUser)}</div>
|
||||
<div>
|
||||
{getProfileImage({
|
||||
...userDetails,
|
||||
userId: userDetails.id,
|
||||
achievements: sortedAchievements,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -2,7 +2,8 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const HERO_HEIGHT = 300;
|
||||
export const HERO_HEIGHT = 150;
|
||||
export const LOGO_HEIGHT = 100;
|
||||
|
||||
export const wrapper = style({
|
||||
display: "flex",
|
||||
@ -38,6 +39,7 @@ export const heroImage = style({
|
||||
transition: "all ease 0.2s",
|
||||
position: "absolute",
|
||||
zIndex: "0",
|
||||
filter: "blur(5px)",
|
||||
"@media": {
|
||||
"(min-width: 1250px)": {
|
||||
objectPosition: "center",
|
||||
@ -53,11 +55,11 @@ export const heroContent = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const gameLogo = style({
|
||||
width: 300,
|
||||
height: LOGO_HEIGHT,
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
@ -72,14 +74,10 @@ export const container = style({
|
||||
export const panel = recipe({
|
||||
base: {
|
||||
width: "100%",
|
||||
height: "150px",
|
||||
minHeight: "150px",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
transition: "all ease 0.2s",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
position: "sticky",
|
||||
overflow: "hidden",
|
||||
top: "0",
|
||||
zIndex: "1",
|
||||
},
|
||||
@ -149,7 +147,6 @@ export const achievementsProgressBar = style({
|
||||
export const heroLogoBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)",
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -184,6 +181,18 @@ export const listItemSkeleton = style({
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "54px",
|
||||
width: "54px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const profileAvatarSmall = style({
|
||||
height: "32px",
|
||||
width: "32px",
|
||||
borderRadius: "4px",
|
||||
|
Loading…
Reference in New Issue
Block a user