mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
refactor: migrate profile page styles from VE to SCSS + BEM
This commit is contained in:
parent
c44b5fa6af
commit
e457950761
@ -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";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
||||
import * as yup from "yup";
|
||||
|
||||
import * as styles from "./edit-profile-modal.css";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import "./edit-profile-modal.scss";
|
||||
|
||||
interface FormValues {
|
||||
profileImageUrl?: string;
|
||||
@ -80,20 +79,9 @@ export function EditProfileModal(
|
||||
<Modal {...props} title={t("edit_profile")} clickOutsideToClose={false}>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
width: "350px",
|
||||
}}
|
||||
className="edit-profile-modal__form"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className="edit-profile-modal__content">
|
||||
<Controller
|
||||
control={control}
|
||||
name="profileImageUrl"
|
||||
@ -140,7 +128,7 @@ export function EditProfileModal(
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarEditContainer}
|
||||
className="edit-profile-modal__avatar-container"
|
||||
onClick={handleChangeProfileAvatar}
|
||||
>
|
||||
<Avatar
|
||||
@ -149,7 +137,7 @@ export function EditProfileModal(
|
||||
alt={userDetails?.displayName}
|
||||
/>
|
||||
|
||||
<div className={styles.profileAvatarEditOverlay}>
|
||||
<div className="edit-profile-modal__avatar-overlay">
|
||||
<DeviceCameraIcon size={38} />
|
||||
</div>
|
||||
</button>
|
||||
@ -167,7 +155,7 @@ export function EditProfileModal(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<small style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
||||
<small className="edit-profile-modal__hint">
|
||||
<Trans i18nKey="privacy_hint" ns="user_profile">
|
||||
<Link to="/settings" />
|
||||
</Trans>
|
||||
@ -175,7 +163,7 @@ export function EditProfileModal(
|
||||
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
style={{ alignSelf: "end", marginTop: `${SPACING_UNIT * 3}px` }}
|
||||
className="edit-profile-modal__submit"
|
||||
type="submit"
|
||||
>
|
||||
{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 { useTranslation } from "react-i18next";
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import * as styles from "./profile-content.css";
|
||||
import { Avatar, Link } from "@renderer/components";
|
||||
import "./friends-box.scss";
|
||||
|
||||
export function FriendsBox() {
|
||||
const { userProfile, userStats } = useContext(userProfileContext);
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const getGameImage = (game: { iconUrl: string | null; title: string }) => {
|
||||
@ -32,15 +30,15 @@ export function FriendsBox() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className="friends-box__section-header">
|
||||
<h2>{t("friends")}</h2>
|
||||
{userStats && (
|
||||
<span>{numberFormatter.format(userStats.friendsCount)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
<div className="friends-box__box">
|
||||
<ul className="friends-box__list">
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li
|
||||
key={friend.id}
|
||||
@ -50,21 +48,22 @@ export function FriendsBox() {
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Link to={`/profile/${friend.id}`} className={styles.listItem}>
|
||||
<Link
|
||||
to={`/profile/${friend.id}`}
|
||||
className="friends-box__list-item"
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
||||
>
|
||||
<span className={styles.friendName}>
|
||||
<div className="friends-box__friend-details">
|
||||
<span className="friends-box__friend-name">
|
||||
{friend.displayName}
|
||||
</span>
|
||||
{friend.currentGame && (
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
<div className="friends-box__game-info">
|
||||
{getGameImage(friend.currentGame)}
|
||||
<small>{friend.currentGame.title}</small>
|
||||
</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 { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./locked-profile.css";
|
||||
import "./locked-profile.scss";
|
||||
|
||||
export function LockedProfile() {
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.lockIcon}>
|
||||
<div className="locked-profile__container">
|
||||
<div className="locked-profile__lock-icon">
|
||||
<LockIcon size={24} />
|
||||
</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 { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -14,6 +12,7 @@ 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 "./profile-content.scss";
|
||||
|
||||
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||
|
||||
@ -84,21 +83,14 @@ export function ProfileContent() {
|
||||
}
|
||||
|
||||
const hasGames = userProfile?.libraryGames.length > 0;
|
||||
|
||||
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<section className="profile-content__section">
|
||||
<div className="profile-content__main">
|
||||
{!hasGames && (
|
||||
<div className={styles.noGames}>
|
||||
<div className={styles.telescopeIcon}>
|
||||
<div className="profile-content__no-games">
|
||||
<div className="profile-content__telescope-icon">
|
||||
<TelescopeIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_recent_activity_title")}</h2>
|
||||
@ -108,15 +100,14 @@ export function ProfileContent() {
|
||||
|
||||
{hasGames && (
|
||||
<>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className="profile-content__section-header">
|
||||
<h2>{t("library")}</h2>
|
||||
|
||||
{userStats && (
|
||||
<span>{numberFormatter.format(userStats.libraryCount)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className={styles.gamesGrid}>
|
||||
<ul className="profile-content__games-grid">
|
||||
{userProfile?.libraryGames?.map((game) => (
|
||||
<UserLibraryGameCard
|
||||
game={game}
|
||||
@ -132,7 +123,7 @@ export function ProfileContent() {
|
||||
</div>
|
||||
|
||||
{shouldShowRightContent && (
|
||||
<div className={styles.rightContent}>
|
||||
<div className="profile-content__right-content">
|
||||
<UserStatsBox />
|
||||
<RecentGamesBox />
|
||||
<FriendsBox />
|
||||
@ -155,7 +146,6 @@ export function ProfileContent() {
|
||||
return (
|
||||
<div>
|
||||
<ProfileHero />
|
||||
|
||||
{content}
|
||||
</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 * as styles from "./profile-content.css";
|
||||
import { Link } from "@renderer/components";
|
||||
import { useCallback, useContext } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
@ -9,12 +7,11 @@ import { ClockIcon } from "@primer/octicons-react";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import type { UserGame } from "@types";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import "./recent-games-box.scss";
|
||||
|
||||
export function RecentGamesBox() {
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
@ -44,28 +41,28 @@ export function RecentGamesBox() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className="recent-games__section-header">
|
||||
<h2>{t("activity")}</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
<div className="recent-games__box">
|
||||
<ul className="recent-games__list">
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className={styles.listItem}
|
||||
className="recent-games__list-item"
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl!}
|
||||
alt={game.title}
|
||||
className={styles.listItemImage}
|
||||
className="recent-games__game-image"
|
||||
/>
|
||||
|
||||
<div className={styles.listItemDetails}>
|
||||
<span className={styles.listItemTitle}>{game.title}</span>
|
||||
<div className="recent-games__game-details">
|
||||
<span className="recent-games__game-title">{game.title}</span>
|
||||
|
||||
<div className={styles.listItemDescription}>
|
||||
<div className="recent-games__game-description">
|
||||
<ClockIcon />
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
</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 * as styles from "./profile-content.css";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -10,11 +9,11 @@ import {
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import "./user-library-game-card.scss";
|
||||
|
||||
interface UserLibraryGameCardProps {
|
||||
game: UserGame;
|
||||
@ -62,9 +61,7 @@ export function UserLibraryGameCard({
|
||||
|
||||
const formatAchievementPoints = (number: number) => {
|
||||
if (number < 100_000) return numberFormatter.format(number);
|
||||
|
||||
if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`;
|
||||
|
||||
return `${(number / 1_000_000).toFixed(1)}M`;
|
||||
};
|
||||
|
||||
@ -88,83 +85,27 @@ export function UserLibraryGameCard({
|
||||
<li
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
}}
|
||||
className="user-library-game__wrapper"
|
||||
title={game.title}
|
||||
className={styles.game}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className={styles.gameCover}
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<div className="user-library-game__overlay">
|
||||
<small className="user-library-game__playtime">
|
||||
<ClockIcon size={11} />
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</small>
|
||||
|
||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
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 className="user-library-game__stats">
|
||||
<div className="user-library-game__stats-header">
|
||||
<div className="user-library-game__stats-content">
|
||||
<div
|
||||
className={styles.gameCardStats}
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
@ -176,12 +117,9 @@ export function UserLibraryGameCard({
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className={styles.gameCardStats}
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 5,
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
@ -203,7 +141,7 @@ export function UserLibraryGameCard({
|
||||
<progress
|
||||
max={1}
|
||||
value={game.unlockedAchievementCount / game.achievementCount}
|
||||
className={styles.achievementsProgressBar}
|
||||
className="user-library-game__achievements-progress"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -212,14 +150,7 @@ export function UserLibraryGameCard({
|
||||
<img
|
||||
src={steamUrlBuilder.cover(game.objectId)}
|
||||
alt={game.title}
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minWidth: "100%",
|
||||
minHeight: "100%",
|
||||
}}
|
||||
className="user-library-game__game-image"
|
||||
/>
|
||||
</button>
|
||||
</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 { userProfileContext } from "@renderer/context";
|
||||
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 { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import "./user-stats-box.scss";
|
||||
|
||||
export function UserStatsBox() {
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
@ -36,22 +35,20 @@ export function UserStatsBox() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className="user-stats__section-header">
|
||||
<h2>{t("stats")}</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
<div className="user-stats__box">
|
||||
<ul className="user-stats__list">
|
||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">
|
||||
{t("achievements_unlocked")}
|
||||
</h3>
|
||||
{userStats.unlockedAchievementSum !== undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<p className={styles.listItemDescription}>
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||
{t("achievements")}
|
||||
</p>
|
||||
@ -60,9 +57,9 @@ export function UserStatsBox() {
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</small>
|
||||
</button>
|
||||
@ -71,13 +68,11 @@ export function UserStatsBox() {
|
||||
)}
|
||||
|
||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>{t("earned_points")}</h3>
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("earned_points")}</h3>
|
||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<p className={styles.listItemDescription}>
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<HydraIcon width={20} height={20} />
|
||||
{numberFormatter.format(
|
||||
userStats.achievementsPointsEarnedSum.value
|
||||
@ -94,9 +89,9 @@ export function UserStatsBox() {
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</small>
|
||||
</button>
|
||||
@ -104,10 +99,10 @@ export function UserStatsBox() {
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className={styles.statsListItem}>
|
||||
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p className={styles.listItemDescription}>
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("total_play_time")}</h3>
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<ClockIcon />
|
||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||
</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 { userProfileContext } from "@renderer/context";
|
||||
import {
|
||||
@ -27,14 +24,12 @@ import type { FriendRequestAction } from "@types";
|
||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||
import "./profile-hero.scss";
|
||||
|
||||
type FriendAction =
|
||||
| FriendRequestAction
|
||||
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
|
||||
|
||||
const backgroundImageLayer =
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 40%), rgb(0 0 0 / 30%))";
|
||||
|
||||
export function ProfileHero() {
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||
@ -127,7 +122,7 @@ export function ProfileHero() {
|
||||
theme="outline"
|
||||
onClick={() => setShowEditProfileModal(true)}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
className="profile-hero__button--outline"
|
||||
>
|
||||
<PencilIcon />
|
||||
{t("edit_profile")}
|
||||
@ -152,7 +147,7 @@ export function ProfileHero() {
|
||||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
className="profile-hero__button--outline"
|
||||
>
|
||||
<PersonAddIcon />
|
||||
{t("add_friend")}
|
||||
@ -187,7 +182,7 @@ export function ProfileHero() {
|
||||
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
className="profile-hero__button--outline"
|
||||
>
|
||||
<XCircleFillIcon />
|
||||
{t("undo_friendship")}
|
||||
@ -201,10 +196,10 @@ export function ProfileHero() {
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
handleFriendAction(userProfile.relation!.AId, "CANCEL")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
className="profile-hero__button--outline"
|
||||
>
|
||||
<XCircleFillIcon /> {t("cancel_request")}
|
||||
</Button>
|
||||
@ -219,7 +214,7 @@ export function ProfileHero() {
|
||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
className="profile-hero__button--outline"
|
||||
>
|
||||
<CheckCircleFillIcon /> {t("accept_request")}
|
||||
</Button>
|
||||
@ -279,34 +274,28 @@ export function ProfileHero() {
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{ background: heroBackground }}
|
||||
className="profile-hero__content-box"
|
||||
style={{ background: !backgroundImage ? heroBackground : undefined }}
|
||||
>
|
||||
{backgroundImage && (
|
||||
<img
|
||||
src={backgroundImage}
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
className="profile-hero__background-image"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: backgroundImage ? backgroundImageLayer : "transparent",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 1,
|
||||
}}
|
||||
className={`profile-hero__background-overlay ${
|
||||
!backgroundImage
|
||||
? "profile-hero__background-overlay--transparent"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.userInformation}>
|
||||
<div className="profile-hero__user-information">
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarButton}
|
||||
className="profile-hero__avatar-button"
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
<Avatar
|
||||
@ -316,9 +305,9 @@ export function ProfileHero() {
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
<div className="profile-hero__information">
|
||||
{userProfile ? (
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
<h2 className="profile-hero__display-name">
|
||||
{userProfile?.displayName}
|
||||
</h2>
|
||||
) : (
|
||||
@ -326,8 +315,8 @@ export function ProfileHero() {
|
||||
)}
|
||||
|
||||
{currentGame && (
|
||||
<div className={styles.currentGameWrapper}>
|
||||
<div className={styles.currentGameDetails}>
|
||||
<div className="profile-hero__current-game-wrapper">
|
||||
<div className="profile-hero__current-game-details">
|
||||
<Link
|
||||
to={buildGameDetailsPath({
|
||||
...currentGame,
|
||||
@ -358,21 +347,14 @@ export function ProfileHero() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.heroPanel}
|
||||
className={`profile-hero__hero-panel ${
|
||||
!backgroundImage ? "profile-hero__hero-panel--transparent" : ""
|
||||
}`}
|
||||
style={{
|
||||
background: backgroundImage ? backgroundImageLayer : heroBackground,
|
||||
background: !backgroundImage ? heroBackground : undefined,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "flex-end",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{profileActions}
|
||||
</div>
|
||||
<div className="profile-hero__actions">{profileActions}</div>
|
||||
</div>
|
||||
</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 { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile.css";
|
||||
import { UserProfileContextProvider } from "@renderer/context";
|
||||
import { useParams } from "react-router-dom";
|
||||
import "./profile.scss";
|
||||
|
||||
export default function Profile() {
|
||||
const { userId } = useParams();
|
||||
|
||||
return (
|
||||
<UserProfileContextProvider userId={userId!}>
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.wrapper}>
|
||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||
<div className="profile__wrapper">
|
||||
<ProfileContent />
|
||||
</div>
|
||||
</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 * as styles from "./report-profile.css";
|
||||
import { Button, Modal, SelectField, TextField } from "@renderer/components";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as yup from "yup";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import "./report-profile.scss";
|
||||
|
||||
const reportReasons = ["hate", "sexual_content", "violence", "spam", "other"];
|
||||
|
||||
@ -75,13 +74,7 @@ export function ReportProfile() {
|
||||
title={t("report_profile")}
|
||||
clickOutsideToClose={false}
|
||||
>
|
||||
<form
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<form className="report-profile__form">
|
||||
<Controller
|
||||
control={control}
|
||||
name="reason"
|
||||
@ -109,7 +102,7 @@ export function ReportProfile() {
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ marginTop: `${SPACING_UNIT}px`, alignSelf: "flex-end" }}
|
||||
className="report-profile__submit"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
{t("report")}
|
||||
@ -119,7 +112,7 @@ export function ReportProfile() {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.reportButton}
|
||||
className="report-profile__button"
|
||||
onClick={() => setShowReportProfileModal(true)}
|
||||
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 { useContext, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
|
||||
import * as styles from "./upload-background-image-button.css";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./upload-background-image-button.scss";
|
||||
|
||||
export function UploadBackgroundImageButton() {
|
||||
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
|
||||
useState(false);
|
||||
const { hasActiveSubscription } = useUserDetails();
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
||||
const { patchUser, fetchUserDetails } = useUserDetails();
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const handleChangeCoverClick = async () => {
|
||||
@ -52,7 +48,7 @@ export function UploadBackgroundImageButton() {
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
className={styles.uploadBackgroundImageButton}
|
||||
className="upload-background-image-button"
|
||||
onClick={handleChangeCoverClick}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user