mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
refactor: migrate profile page styles from VE to SCSS + BEM
This commit is contained in:
parent
c44b5fa6af
commit
e457950761
22 changed files with 786 additions and 237 deletions
|
@ -0,0 +1,57 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.edit-profile-modal {
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
margin-top: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
align-self: end;
|
||||||
|
margin-top: calc(globals.$spacing-unit * 3);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar-container {
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
color: globals.$body-color;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: globals.$muted-color;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar-container:hover &__avatar-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,13 +13,12 @@ import {
|
||||||
} from "@renderer/components";
|
} from "@renderer/components";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import * as styles from "./edit-profile-modal.css";
|
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
import "./edit-profile-modal.scss";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
|
@ -80,20 +79,9 @@ export function EditProfileModal(
|
||||||
<Modal {...props} title={t("edit_profile")} clickOutsideToClose={false}>
|
<Modal {...props} title={t("edit_profile")} clickOutsideToClose={false}>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
style={{
|
className="edit-profile-modal__form"
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "350px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="edit-profile-modal__content">
|
||||||
style={{
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="profileImageUrl"
|
name="profileImageUrl"
|
||||||
|
@ -140,7 +128,7 @@ export function EditProfileModal(
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileAvatarEditContainer}
|
className="edit-profile-modal__avatar-container"
|
||||||
onClick={handleChangeProfileAvatar}
|
onClick={handleChangeProfileAvatar}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -149,7 +137,7 @@ export function EditProfileModal(
|
||||||
alt={userDetails?.displayName}
|
alt={userDetails?.displayName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.profileAvatarEditOverlay}>
|
<div className="edit-profile-modal__avatar-overlay">
|
||||||
<DeviceCameraIcon size={38} />
|
<DeviceCameraIcon size={38} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -167,7 +155,7 @@ export function EditProfileModal(
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
<small className="edit-profile-modal__hint">
|
||||||
<Trans i18nKey="privacy_hint" ns="user_profile">
|
<Trans i18nKey="privacy_hint" ns="user_profile">
|
||||||
<Link to="/settings" />
|
<Link to="/settings" />
|
||||||
</Trans>
|
</Trans>
|
||||||
|
@ -175,7 +163,7 @@ export function EditProfileModal(
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
style={{ alignSelf: "end", marginTop: `${SPACING_UNIT * 3}px` }}
|
className="edit-profile-modal__submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{isSubmitting ? t("saving") : t("save")}
|
{isSubmitting ? t("saving") : t("save")}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.friends-box {
|
||||||
|
&__section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__box {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friend-name {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-info {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friend-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,12 @@ import { useFormat } from "@renderer/hooks";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import * as styles from "./profile-content.css";
|
|
||||||
import { Avatar, Link } from "@renderer/components";
|
import { Avatar, Link } from "@renderer/components";
|
||||||
|
import "./friends-box.scss";
|
||||||
|
|
||||||
export function FriendsBox() {
|
export function FriendsBox() {
|
||||||
const { userProfile, userStats } = useContext(userProfileContext);
|
const { userProfile, userStats } = useContext(userProfileContext);
|
||||||
|
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const getGameImage = (game: { iconUrl: string | null; title: string }) => {
|
const getGameImage = (game: { iconUrl: string | null; title: string }) => {
|
||||||
|
@ -32,15 +30,15 @@ export function FriendsBox() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="friends-box__section-header">
|
||||||
<h2>{t("friends")}</h2>
|
<h2>{t("friends")}</h2>
|
||||||
{userStats && (
|
{userStats && (
|
||||||
<span>{numberFormatter.format(userStats.friendsCount)}</span>
|
<span>{numberFormatter.format(userStats.friendsCount)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className="friends-box__box">
|
||||||
<ul className={styles.list}>
|
<ul className="friends-box__list">
|
||||||
{userProfile?.friends.map((friend) => (
|
{userProfile?.friends.map((friend) => (
|
||||||
<li
|
<li
|
||||||
key={friend.id}
|
key={friend.id}
|
||||||
|
@ -50,21 +48,22 @@ export function FriendsBox() {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link to={`/profile/${friend.id}`} className={styles.listItem}>
|
<Link
|
||||||
|
to={`/profile/${friend.id}`}
|
||||||
|
className="friends-box__list-item"
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={32}
|
size={32}
|
||||||
src={friend.profileImageUrl}
|
src={friend.profileImageUrl}
|
||||||
alt={friend.displayName}
|
alt={friend.displayName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div className="friends-box__friend-details">
|
||||||
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
<span className="friends-box__friend-name">
|
||||||
>
|
|
||||||
<span className={styles.friendName}>
|
|
||||||
{friend.displayName}
|
{friend.displayName}
|
||||||
</span>
|
</span>
|
||||||
{friend.currentGame && (
|
{friend.currentGame && (
|
||||||
<div style={{ display: "flex", gap: 4 }}>
|
<div className="friends-box__game-info">
|
||||||
{getGameImage(friend.currentGame)}
|
{getGameImage(friend.currentGame)}
|
||||||
<small>{friend.currentGame.title}</small>
|
<small>{friend.currentGame.title}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.locked-profile {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lock-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
import { LockIcon } from "@primer/octicons-react";
|
import { LockIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./locked-profile.scss";
|
||||||
import * as styles from "./locked-profile.css";
|
|
||||||
|
|
||||||
export function LockedProfile() {
|
export function LockedProfile() {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="locked-profile__container">
|
||||||
<div className={styles.lockIcon}>
|
<div className="locked-profile__lock-icon">
|
||||||
<LockIcon size={24} />
|
<LockIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
padding: calc(globals.$spacing-unit * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
max-width: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-games {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__telescope-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__games-grid {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 900px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1300px) {
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 2000px) {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 2600px) {
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 3000px) {
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,6 @@ 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 { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import * as styles from "./profile-content.css";
|
|
||||||
import { TelescopeIcon } from "@primer/octicons-react";
|
import { TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
@ -14,6 +12,7 @@ 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 "./profile-content.scss";
|
||||||
|
|
||||||
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||||
|
|
||||||
|
@ -84,21 +83,14 @@ export function ProfileContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasGames = userProfile?.libraryGames.length > 0;
|
const hasGames = userProfile?.libraryGames.length > 0;
|
||||||
|
|
||||||
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
|
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section className="profile-content__section">
|
||||||
style={{
|
<div className="profile-content__main">
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
padding: `${SPACING_UNIT * 3}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
{!hasGames && (
|
{!hasGames && (
|
||||||
<div className={styles.noGames}>
|
<div className="profile-content__no-games">
|
||||||
<div className={styles.telescopeIcon}>
|
<div className="profile-content__telescope-icon">
|
||||||
<TelescopeIcon size={24} />
|
<TelescopeIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
|
@ -108,15 +100,14 @@ export function ProfileContent() {
|
||||||
|
|
||||||
{hasGames && (
|
{hasGames && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="profile-content__section-header">
|
||||||
<h2>{t("library")}</h2>
|
<h2>{t("library")}</h2>
|
||||||
|
|
||||||
{userStats && (
|
{userStats && (
|
||||||
<span>{numberFormatter.format(userStats.libraryCount)}</span>
|
<span>{numberFormatter.format(userStats.libraryCount)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className={styles.gamesGrid}>
|
<ul className="profile-content__games-grid">
|
||||||
{userProfile?.libraryGames?.map((game) => (
|
{userProfile?.libraryGames?.map((game) => (
|
||||||
<UserLibraryGameCard
|
<UserLibraryGameCard
|
||||||
game={game}
|
game={game}
|
||||||
|
@ -132,7 +123,7 @@ export function ProfileContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldShowRightContent && (
|
{shouldShowRightContent && (
|
||||||
<div className={styles.rightContent}>
|
<div className="profile-content__right-content">
|
||||||
<UserStatsBox />
|
<UserStatsBox />
|
||||||
<RecentGamesBox />
|
<RecentGamesBox />
|
||||||
<FriendsBox />
|
<FriendsBox />
|
||||||
|
@ -155,7 +146,6 @@ export function ProfileContent() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProfileHero />
|
<ProfileHero />
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.recent-games {
|
||||||
|
&__box {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-image {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-title {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { useCallback, useContext } from "react";
|
import { useCallback, useContext } from "react";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
@ -9,12 +7,11 @@ import { ClockIcon } from "@primer/octicons-react";
|
||||||
import { useFormat } from "@renderer/hooks";
|
import { useFormat } from "@renderer/hooks";
|
||||||
import type { UserGame } from "@types";
|
import type { UserGame } from "@types";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import "./recent-games-box.scss";
|
||||||
|
|
||||||
export function RecentGamesBox() {
|
export function RecentGamesBox() {
|
||||||
const { userProfile } = useContext(userProfileContext);
|
const { userProfile } = useContext(userProfileContext);
|
||||||
|
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const formatPlayTime = useCallback(
|
const formatPlayTime = useCallback(
|
||||||
|
@ -44,28 +41,28 @@ export function RecentGamesBox() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="recent-games__section-header">
|
||||||
<h2>{t("activity")}</h2>
|
<h2>{t("activity")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className="recent-games__box">
|
||||||
<ul className={styles.list}>
|
<ul className="recent-games__list">
|
||||||
{userProfile?.recentGames.map((game) => (
|
{userProfile?.recentGames.map((game) => (
|
||||||
<li key={`${game.shop}-${game.objectId}`}>
|
<li key={`${game.shop}-${game.objectId}`}>
|
||||||
<Link
|
<Link
|
||||||
to={buildUserGameDetailsPath(game)}
|
to={buildUserGameDetailsPath(game)}
|
||||||
className={styles.listItem}
|
className="recent-games__list-item"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={game.iconUrl!}
|
src={game.iconUrl!}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className={styles.listItemImage}
|
className="recent-games__game-image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.listItemDetails}>
|
<div className="recent-games__game-details">
|
||||||
<span className={styles.listItemTitle}>{game.title}</span>
|
<span className="recent-games__game-title">{game.title}</span>
|
||||||
|
|
||||||
<div className={styles.listItemDescription}>
|
<div className="recent-games__game-description">
|
||||||
<ClockIcon />
|
<ClockIcon />
|
||||||
<small>{formatPlayTime(game)}</small>
|
<small>{formatPlayTime(game)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.user-library-game {
|
||||||
|
&__wrapper {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
box-shadow: 0 8px 10px -2px rgba(0, 0, 0, 0.5);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 172%;
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(
|
||||||
|
35deg,
|
||||||
|
rgba(0, 0, 0, 0.1) 0%,
|
||||||
|
rgba(0, 0, 0, 0.07) 51.5%,
|
||||||
|
rgba(255, 255, 255, 0.15) 54%,
|
||||||
|
rgba(255, 255, 255, 0.15) 100%
|
||||||
|
);
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
transform: translateY(-36%);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__overlay {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__playtime {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-image {
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__achievements-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: globals.$muted-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
import * as styles from "./profile-content.css";
|
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { useFormat } from "@renderer/hooks";
|
import { useFormat } from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
@ -10,11 +9,11 @@ import {
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
import "./user-library-game-card.scss";
|
||||||
|
|
||||||
interface UserLibraryGameCardProps {
|
interface UserLibraryGameCardProps {
|
||||||
game: UserGame;
|
game: UserGame;
|
||||||
|
@ -62,9 +61,7 @@ export function UserLibraryGameCard({
|
||||||
|
|
||||||
const formatAchievementPoints = (number: number) => {
|
const formatAchievementPoints = (number: number) => {
|
||||||
if (number < 100_000) return numberFormatter.format(number);
|
if (number < 100_000) return numberFormatter.format(number);
|
||||||
|
|
||||||
if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`;
|
if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`;
|
||||||
|
|
||||||
return `${(number / 1_000_000).toFixed(1)}M`;
|
return `${(number / 1_000_000).toFixed(1)}M`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,83 +85,27 @@ export function UserLibraryGameCard({
|
||||||
<li
|
<li
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
style={{
|
className="user-library-game__wrapper"
|
||||||
borderRadius: 4,
|
|
||||||
overflow: "hidden",
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
title={game.title}
|
title={game.title}
|
||||||
className={styles.game}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
className="user-library-game__cover"
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
className={styles.gameCover}
|
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
>
|
>
|
||||||
<div
|
<div className="user-library-game__overlay">
|
||||||
style={{
|
<small className="user-library-game__playtime">
|
||||||
position: "absolute",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
background:
|
|
||||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.70) 20%, transparent 100%)",
|
|
||||||
padding: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
style={{
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
color: vars.color.muted,
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
borderRadius: 4,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 4,
|
|
||||||
padding: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ClockIcon size={11} />
|
<ClockIcon size={11} />
|
||||||
{formatPlayTime(game.playTimeInSeconds)}
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||||
<div
|
<div className="user-library-game__stats">
|
||||||
style={{
|
<div className="user-library-game__stats-header">
|
||||||
width: "100%",
|
<div className="user-library-game__stats-content">
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
overflow: "hidden",
|
|
||||||
height: 18,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={styles.gameCardStats}
|
className="user-library-game__stats-item"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -176,12 +117,9 @@ export function UserLibraryGameCard({
|
||||||
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
{game.achievementsPointsEarnedSum > 0 && (
|
||||||
<div
|
<div
|
||||||
className={styles.gameCardStats}
|
className="user-library-game__stats-item"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
gap: 5,
|
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HydraIcon width={16} height={16} />
|
<HydraIcon width={16} height={16} />
|
||||||
|
@ -203,7 +141,7 @@ export function UserLibraryGameCard({
|
||||||
<progress
|
<progress
|
||||||
max={1}
|
max={1}
|
||||||
value={game.unlockedAchievementCount / game.achievementCount}
|
value={game.unlockedAchievementCount / game.achievementCount}
|
||||||
className={styles.achievementsProgressBar}
|
className="user-library-game__achievements-progress"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -212,14 +150,7 @@ export function UserLibraryGameCard({
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.cover(game.objectId)}
|
src={steamUrlBuilder.cover(game.objectId)}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
style={{
|
className="user-library-game__game-image"
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: 4,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
minWidth: "100%",
|
|
||||||
minHeight: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
&__box {
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-title {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
text-align: start;
|
||||||
|
color: globals.$body-color;
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
color: globals.$warning-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import * as styles from "./profile-content.css";
|
|
||||||
import { useCallback, useContext } from "react";
|
import { useCallback, useContext } from "react";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -7,7 +6,7 @@ import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { vars } from "@renderer/theme.css";
|
import "./user-stats-box.scss";
|
||||||
|
|
||||||
export function UserStatsBox() {
|
export function UserStatsBox() {
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
|
@ -36,22 +35,20 @@ export function UserStatsBox() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="user-stats__section-header">
|
||||||
<h2>{t("stats")}</h2>
|
<h2>{t("stats")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className="user-stats__box">
|
||||||
<ul className={styles.list}>
|
<ul className="user-stats__list">
|
||||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||||
<li className={styles.statsListItem}>
|
<li className="user-stats__list-item">
|
||||||
<h3 className={styles.listItemTitle}>
|
<h3 className="user-stats__list-title">
|
||||||
{t("achievements_unlocked")}
|
{t("achievements_unlocked")}
|
||||||
</h3>
|
</h3>
|
||||||
{userStats.unlockedAchievementSum !== undefined ? (
|
{userStats.unlockedAchievementSum !== undefined ? (
|
||||||
<div
|
<div className="user-stats__stats-row">
|
||||||
style={{ display: "flex", justifyContent: "space-between" }}
|
<p className="user-stats__list-description">
|
||||||
>
|
|
||||||
<p className={styles.listItemDescription}>
|
|
||||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||||
{t("achievements")}
|
{t("achievements")}
|
||||||
</p>
|
</p>
|
||||||
|
@ -60,9 +57,9 @@ export function UserStatsBox() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
className={styles.link}
|
className="user-stats__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small style={{ color: "var(--color-warning)" }}>
|
||||||
{t("show_achievements_on_profile")}
|
{t("show_achievements_on_profile")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
|
@ -71,13 +68,11 @@ export function UserStatsBox() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||||
<li className={styles.statsListItem}>
|
<li className="user-stats__list-item">
|
||||||
<h3 className={styles.listItemTitle}>{t("earned_points")}</h3>
|
<h3 className="user-stats__list-title">{t("earned_points")}</h3>
|
||||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||||
<div
|
<div className="user-stats__stats-row">
|
||||||
style={{ display: "flex", justifyContent: "space-between" }}
|
<p className="user-stats__list-description">
|
||||||
>
|
|
||||||
<p className={styles.listItemDescription}>
|
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon width={20} height={20} />
|
||||||
{numberFormatter.format(
|
{numberFormatter.format(
|
||||||
userStats.achievementsPointsEarnedSum.value
|
userStats.achievementsPointsEarnedSum.value
|
||||||
|
@ -94,9 +89,9 @@ export function UserStatsBox() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements-points")}
|
onClick={() => showHydraCloudModal("achievements-points")}
|
||||||
className={styles.link}
|
className="user-stats__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small className="user-stats__link--warning">
|
||||||
{t("show_points_on_profile")}
|
{t("show_points_on_profile")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
|
@ -104,10 +99,10 @@ export function UserStatsBox() {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<li className={styles.statsListItem}>
|
<li className="user-stats__list-item">
|
||||||
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
|
<h3 className="user-stats__list-title">{t("total_play_time")}</h3>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div className="user-stats__stats-row">
|
||||||
<p className={styles.listItemDescription}>
|
<p className="user-stats__list-description">
|
||||||
<ClockIcon />
|
<ClockIcon />
|
||||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||||
</p>
|
</p>
|
||||||
|
|
124
src/renderer/src/pages/profile/profile-hero/profile-hero.scss
Normal file
124
src/renderer/src/pages/profile/profile-hero/profile-hero.scss
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.profile-hero {
|
||||||
|
&__content-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__background {
|
||||||
|
&-image {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
background: linear-gradient(135deg, rgb(0 0 0 / 40%), rgb(0 0 0 / 30%));
|
||||||
|
|
||||||
|
&--transparent {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__user-information {
|
||||||
|
display: flex;
|
||||||
|
padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3);
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar-button {
|
||||||
|
width: 96px;
|
||||||
|
min-width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: globals.$background-color;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: flex-start;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__display-name {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
text-shadow: 0 0 5px rgb(0 0 0 / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current-game {
|
||||||
|
&-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-panel {
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
justify-content: space-between;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-top: solid 1px rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&--transparent {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
&--outline {
|
||||||
|
border-color: globals.$body-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,3 @@
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./profile-hero.css";
|
|
||||||
import { useCallback, useContext, useMemo, useState } from "react";
|
import { useCallback, useContext, useMemo, useState } from "react";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import {
|
import {
|
||||||
|
@ -27,14 +24,12 @@ import type { FriendRequestAction } from "@types";
|
||||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||||
|
import "./profile-hero.scss";
|
||||||
|
|
||||||
type FriendAction =
|
type FriendAction =
|
||||||
| FriendRequestAction
|
| FriendRequestAction
|
||||||
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
||||||
|
|
||||||
const backgroundImageLayer =
|
|
||||||
"linear-gradient(135deg, rgb(0 0 0 / 40%), rgb(0 0 0 / 30%))";
|
|
||||||
|
|
||||||
export function ProfileHero() {
|
export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||||
|
@ -127,7 +122,7 @@ export function ProfileHero() {
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => setShowEditProfileModal(true)}
|
onClick={() => setShowEditProfileModal(true)}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
className="profile-hero__button--outline"
|
||||||
>
|
>
|
||||||
<PencilIcon />
|
<PencilIcon />
|
||||||
{t("edit_profile")}
|
{t("edit_profile")}
|
||||||
|
@ -152,7 +147,7 @@ export function ProfileHero() {
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
className="profile-hero__button--outline"
|
||||||
>
|
>
|
||||||
<PersonAddIcon />
|
<PersonAddIcon />
|
||||||
{t("add_friend")}
|
{t("add_friend")}
|
||||||
|
@ -187,7 +182,7 @@ export function ProfileHero() {
|
||||||
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
className="profile-hero__button--outline"
|
||||||
>
|
>
|
||||||
<XCircleFillIcon />
|
<XCircleFillIcon />
|
||||||
{t("undo_friendship")}
|
{t("undo_friendship")}
|
||||||
|
@ -201,10 +196,10 @@ export function ProfileHero() {
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
handleFriendAction(userProfile.relation!.AId, "CANCEL")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
className="profile-hero__button--outline"
|
||||||
>
|
>
|
||||||
<XCircleFillIcon /> {t("cancel_request")}
|
<XCircleFillIcon /> {t("cancel_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -219,7 +214,7 @@ export function ProfileHero() {
|
||||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
className="profile-hero__button--outline"
|
||||||
>
|
>
|
||||||
<CheckCircleFillIcon /> {t("accept_request")}
|
<CheckCircleFillIcon /> {t("accept_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -279,34 +274,28 @@ export function ProfileHero() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className="profile-hero__content-box"
|
||||||
style={{ background: heroBackground }}
|
style={{ background: !backgroundImage ? heroBackground : undefined }}
|
||||||
>
|
>
|
||||||
{backgroundImage && (
|
{backgroundImage && (
|
||||||
<img
|
<img
|
||||||
src={backgroundImage}
|
src={backgroundImage}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
className="profile-hero__background-image"
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
className={`profile-hero__background-overlay ${
|
||||||
background: backgroundImage ? backgroundImageLayer : "transparent",
|
!backgroundImage
|
||||||
width: "100%",
|
? "profile-hero__background-overlay--transparent"
|
||||||
height: "100%",
|
: ""
|
||||||
zIndex: 1,
|
}`}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={styles.userInformation}>
|
<div className="profile-hero__user-information">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileAvatarButton}
|
className="profile-hero__avatar-button"
|
||||||
onClick={handleAvatarClick}
|
onClick={handleAvatarClick}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -316,9 +305,9 @@ export function ProfileHero() {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.profileInformation}>
|
<div className="profile-hero__information">
|
||||||
{userProfile ? (
|
{userProfile ? (
|
||||||
<h2 className={styles.profileDisplayName}>
|
<h2 className="profile-hero__display-name">
|
||||||
{userProfile?.displayName}
|
{userProfile?.displayName}
|
||||||
</h2>
|
</h2>
|
||||||
) : (
|
) : (
|
||||||
|
@ -326,8 +315,8 @@ export function ProfileHero() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentGame && (
|
{currentGame && (
|
||||||
<div className={styles.currentGameWrapper}>
|
<div className="profile-hero__current-game-wrapper">
|
||||||
<div className={styles.currentGameDetails}>
|
<div className="profile-hero__current-game-details">
|
||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({
|
to={buildGameDetailsPath({
|
||||||
...currentGame,
|
...currentGame,
|
||||||
|
@ -358,21 +347,14 @@ export function ProfileHero() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.heroPanel}
|
className={`profile-hero__hero-panel ${
|
||||||
|
!backgroundImage ? "profile-hero__hero-panel--transparent" : ""
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
background: backgroundImage ? backgroundImageLayer : heroBackground,
|
background: !backgroundImage ? heroBackground : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="profile-hero__actions">{profileActions}</div>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{profileActions}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|
10
src/renderer/src/pages/profile/profile.scss
Normal file
10
src/renderer/src/pages/profile/profile.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
&__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,16 @@
|
||||||
import { ProfileContent } from "./profile-content/profile-content";
|
import { ProfileContent } from "./profile-content/profile-content";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./profile.css";
|
|
||||||
import { UserProfileContextProvider } from "@renderer/context";
|
import { UserProfileContextProvider } from "@renderer/context";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import "./profile.scss";
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const { userId } = useParams();
|
const { userId } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProfileContextProvider userId={userId!}>
|
<UserProfileContextProvider userId={userId!}>
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||||
<div className={styles.wrapper}>
|
<div className="profile__wrapper">
|
||||||
<ProfileContent />
|
<ProfileContent />
|
||||||
</div>
|
</div>
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.report-profile {
|
||||||
|
&__button {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
margin-top: globals.$spacing-unit;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
import { ReportIcon } from "@primer/octicons-react";
|
import { ReportIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./report-profile.css";
|
|
||||||
import { Button, Modal, SelectField, TextField } from "@renderer/components";
|
import { Button, Modal, SelectField, TextField } from "@renderer/components";
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { useToast } from "@renderer/hooks";
|
import { useToast } from "@renderer/hooks";
|
||||||
|
import "./report-profile.scss";
|
||||||
|
|
||||||
const reportReasons = ["hate", "sexual_content", "violence", "spam", "other"];
|
const reportReasons = ["hate", "sexual_content", "violence", "spam", "other"];
|
||||||
|
|
||||||
|
@ -75,13 +74,7 @@ export function ReportProfile() {
|
||||||
title={t("report_profile")}
|
title={t("report_profile")}
|
||||||
clickOutsideToClose={false}
|
clickOutsideToClose={false}
|
||||||
>
|
>
|
||||||
<form
|
<form className="report-profile__form">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="reason"
|
name="reason"
|
||||||
|
@ -109,7 +102,7 @@ export function ReportProfile() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={{ marginTop: `${SPACING_UNIT}px`, alignSelf: "flex-end" }}
|
className="report-profile__submit"
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
{t("report")}
|
{t("report")}
|
||||||
|
@ -119,7 +112,7 @@ export function ReportProfile() {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.reportButton}
|
className="report-profile__button"
|
||||||
onClick={() => setShowReportProfileModal(true)}
|
onClick={() => setShowReportProfileModal(true)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.upload-background-image-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
border-color: globals.$body-color;
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
|
@ -2,21 +2,17 @@ import { UploadIcon } from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
|
||||||
import * as styles from "./upload-background-image-button.css";
|
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./upload-background-image-button.scss";
|
||||||
|
|
||||||
export function UploadBackgroundImageButton() {
|
export function UploadBackgroundImageButton() {
|
||||||
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
|
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const { hasActiveSubscription } = useUserDetails();
|
const { hasActiveSubscription } = useUserDetails();
|
||||||
|
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
||||||
const { patchUser, fetchUserDetails } = useUserDetails();
|
const { patchUser, fetchUserDetails } = useUserDetails();
|
||||||
|
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const handleChangeCoverClick = async () => {
|
const handleChangeCoverClick = async () => {
|
||||||
|
@ -52,7 +48,7 @@ export function UploadBackgroundImageButton() {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
className={styles.uploadBackgroundImageButton}
|
className="upload-background-image-button"
|
||||||
onClick={handleChangeCoverClick}
|
onClick={handleChangeCoverClick}
|
||||||
disabled={isUploadingBackgroundImage}
|
disabled={isUploadingBackgroundImage}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue