mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
migration to scss
This commit is contained in:
parent
691dba26af
commit
b0eb7c16cd
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "./scss/variables";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&--closing {
|
&__closing {
|
||||||
animation-name: scale-fade-out;
|
animation-name: scale-fade-out;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--large {
|
&__large {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
@ -110,8 +110,8 @@ export function Modal({
|
|||||||
<Backdrop isClosing={isClosing}>
|
<Backdrop isClosing={isClosing}>
|
||||||
<div
|
<div
|
||||||
className={cn("modal", {
|
className={cn("modal", {
|
||||||
"modal--closing": isClosing,
|
"modal__closing": isClosing,
|
||||||
"modal--large": large,
|
"modal__large": large,
|
||||||
})}
|
})}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby={title}
|
aria-labelledby={title}
|
||||||
|
@ -13,15 +13,15 @@
|
|||||||
border-color: rgba(255, 255, 255, 0.5);
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--focused {
|
&__focused {
|
||||||
border-color: #dadbe1;
|
border-color: #dadbe1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--primary {
|
&__primary {
|
||||||
background-color: $dark-background-color;
|
background-color: $dark-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--dark {
|
&__dark {
|
||||||
background-color: $background-color;
|
background-color: $background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export function SelectField({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn("select-field", `select-field--${theme}`, {
|
className={cn("select-field", `select-field--${theme}`, {
|
||||||
"select-field--focused": isFocused,
|
"select-field__focused": isFocused,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
|
@ -11,12 +11,12 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: globals.$spacing-unit;
|
padding-top: globals.$spacing-unit;
|
||||||
|
|
||||||
&--resizing {
|
&__resizing {
|
||||||
opacity: globals.$active-opacity;
|
opacity: globals.$active-opacity;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--darwin {
|
&__darwin {
|
||||||
padding-top: calc(globals.$spacing-unit * 6);
|
padding-top: calc(globals.$spacing-unit * 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,8 +171,8 @@ export function Sidebar() {
|
|||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className={cn("sidebar", {
|
className={cn("sidebar", {
|
||||||
"sidebar--resizing": isResizing,
|
"sidebar__resizing": isResizing,
|
||||||
"sidebar--darwin": window.electron.platform === "darwin",
|
"sidebar__darwin": window.electron.platform === "darwin",
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
width: sidebarWidth,
|
width: sidebarWidth,
|
||||||
|
@ -17,19 +17,19 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|
||||||
&--primary {
|
&__primary {
|
||||||
background-color: $dark-background-color;
|
background-color: $dark-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--dark {
|
&__dark {
|
||||||
background-color: $background-color;
|
background-color: $background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--has-error {
|
&__has-error {
|
||||||
border-color: $danger-color;
|
border-color: $danger-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--focused {
|
&__focused {
|
||||||
border-color: $search-border-color-focused;
|
border-color: $search-border-color-focused;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +54,7 @@
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--read-only {
|
&__read-only {
|
||||||
text-overflow: inherit;
|
text-overflow: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,8 +88,8 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
"text-field-container__text-field",
|
"text-field-container__text-field",
|
||||||
`text-field-container__text-field--${theme}`,
|
`text-field-container__text-field--${theme}`,
|
||||||
{
|
{
|
||||||
"text-field-container__text-field--has-error": hasError,
|
"text-field-container__text-field__has-error": hasError,
|
||||||
"text-field-container__text-field--focused": isFocused,
|
"text-field-container__text-field__focused": isFocused,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
@ -98,7 +98,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
className={cn("text-field-container__text-field-input", {
|
className={cn("text-field-container__text-field-input", {
|
||||||
"text-field-container__text-field-input--read-only":
|
"text-field-container__text-field-input__read-only":
|
||||||
props.readOnly,
|
props.readOnly,
|
||||||
})}
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -35,12 +35,12 @@
|
|||||||
z-index: $toast-z-index;
|
z-index: $toast-z-index;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
|
||||||
&--closing {
|
&__closing {
|
||||||
animation-name: slideOut;
|
animation-name: slideOut;
|
||||||
transform: translateY(96px);
|
transform: translateY(96px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--opening {
|
&__opening {
|
||||||
animation-name: slideIn;
|
animation-name: slideIn;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
|
|
||||||
import "./toast.scss";
|
import "./toast.scss";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
@ -80,7 +79,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("toast", {
|
className={cn("toast", {
|
||||||
"toast--closing": isClosing,
|
"toast__closing": isClosing,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="toast__content">
|
<div className="toast__content">
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
import type { UserAchievement } from "@types";
|
import type { UserAchievement } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||||
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 { vars } from "@renderer/theme.css";
|
import "./achievements.scss";
|
||||||
|
import "../../scss/_variables.scss"
|
||||||
|
|
||||||
interface AchievementListProps {
|
interface AchievementListProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
@ -17,16 +17,16 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className="achievements__list">
|
||||||
{achievements.map((achievement) => (
|
{achievements.map((achievement) => (
|
||||||
<li
|
<li
|
||||||
key={achievement.name}
|
key={achievement.name}
|
||||||
className={styles.listItem}
|
className="achievements__list-item"
|
||||||
style={{ display: "flex" }}
|
style={{ display: "flex" }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={classNames("achievements__list-item-image", {
|
||||||
unlocked: achievement.unlocked,
|
"achievements__list-item-image--unlocked": achievement.unlocked,
|
||||||
})}
|
})}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
@ -66,7 +66,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "4px",
|
gap: "4px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: vars.color.warning,
|
color: "var(--warning-color)",
|
||||||
}}
|
}}
|
||||||
title={t("achievement_earn_points", {
|
title={t("achievement_earn_points", {
|
||||||
points: "???",
|
points: "???",
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
background-color: $color-muted;
|
background-color: $color-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--disabled {
|
&__disabled {
|
||||||
opacity: $opacity-disabled;
|
opacity: $opacity-disabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,9 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
|||||||
import { UserAchievement } from "@types";
|
import { UserAchievement } from "@types";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
import * as styles from "./achievement-panel.css";
|
|
||||||
|
import "./achievement-panel.sccs";
|
||||||
|
|
||||||
export interface AchievementPanelProps {
|
export interface AchievementPanelProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
@ -28,8 +29,8 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
|||||||
|
|
||||||
if (!hasActiveSubscription) {
|
if (!hasActiveSubscription) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||||
??? / ???
|
??? / ???
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +39,7 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
|||||||
onClick={() => showHydraCloudModal("achievements-points")}
|
onClick={() => showHydraCloudModal("achievements-points")}
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small style={{ color: "#ffc107" }}>
|
||||||
{t("how_to_earn_achievements_points")}
|
{t("how_to_earn_achievements_points")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
@ -47,8 +48,8 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,7 +139,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
color: vars.color.muted,
|
color: "#c0c1c7",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -253,7 +253,7 @@ export function AchievementsContent({
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={heroRef} className={styles.hero}>
|
<div ref={heroRef} className={styles.hero}>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import * as styles from "./achievements.css";
|
import "./achievements.scss";
|
||||||
|
|
||||||
export function AchievementsSkeleton() {
|
export function AchievementsSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="achievements__container">
|
||||||
<div className={styles.hero}>
|
<div className="achievements__hero">
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="achievements__hero-image-skeleton" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.heroPanelSkeleton}></div>
|
<div className="achievements__hero-panel-skeleton"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
|||||||
import type { ComparedAchievements, GameShop } from "@types";
|
import type { ComparedAchievements, GameShop } from "@types";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
import "./achievements.scss";
|
||||||
import {
|
import {
|
||||||
GameDetailsContextConsumer,
|
GameDetailsContextConsumer,
|
||||||
GameDetailsContextProvider,
|
GameDetailsContextProvider,
|
||||||
@ -76,7 +77,7 @@ export default function Achievements() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme
|
<SkeletonTheme
|
||||||
baseColor={vars.color.background}
|
baseColor="#1c1c1c"
|
||||||
highlightColor="#444"
|
highlightColor="#444"
|
||||||
>
|
>
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import type { ComparedAchievements } from "@types";
|
import type { ComparedAchievements } from "@types";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
EyeClosedIcon,
|
EyeClosedIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./achievements.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface ComparedAchievementListProps {
|
export interface ComparedAchievementListProps {
|
||||||
achievements: ComparedAchievements;
|
achievements: ComparedAchievements;
|
||||||
@ -20,11 +20,11 @@ export function ComparedAchievementList({
|
|||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className="achievements__list">
|
||||||
{achievements.achievements.map((achievement, index) => (
|
{achievements.achievements.map((achievement, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={styles.listItem}
|
className="achievements__list-item"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: achievement.ownerStat
|
gridTemplateColumns: achievement.ownerStat
|
||||||
@ -37,12 +37,14 @@ export function ComparedAchievementList({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={classNames("achievements__list-item-image", {
|
||||||
unlocked: true,
|
"achievements__list-item-image--unlocked":
|
||||||
|
achievement.ownerStat?.unlocked ||
|
||||||
|
achievement.targetStat.unlocked,
|
||||||
})}
|
})}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
@ -71,7 +73,7 @@ export function ComparedAchievementList({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||||
@ -82,7 +84,7 @@ export function ComparedAchievementList({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -97,7 +99,7 @@ export function ComparedAchievementList({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
||||||
@ -108,7 +110,7 @@ export function ComparedAchievementList({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./delete-game-modal.css";
|
import "./delete-game-modal.scss";
|
||||||
|
|
||||||
interface DeleteGameModalProps {
|
interface DeleteGameModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -29,7 +29,8 @@ export function DeleteGameModal({
|
|||||||
description={t("delete_modal_description")}
|
description={t("delete_modal_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.deleteActionsButtonsCtn}>
|
<div className="delete-game-modal__actions-buttons-ctn">
|
||||||
|
|
||||||
<Button onClick={handleDeleteGame} theme="outline">
|
<Button onClick={handleDeleteGame} theme="outline">
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -12,7 +12,9 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
|||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./download-group.css";
|
|
||||||
|
import "./download-group.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -238,7 +240,7 @@ export function DownloadGroup({
|
|||||||
if (!library.length) return null;
|
if (!library.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.downloadGroup}>
|
<div className="download-group">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -259,33 +261,33 @@ export function DownloadGroup({
|
|||||||
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className={styles.downloads}>
|
<ul className="download-group__downloads">
|
||||||
{library.map((game) => {
|
{library.map((game) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.download}
|
className="download-group__download"
|
||||||
style={{ position: "relative" }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
<div className={styles.downloadCover}>
|
<div className="download-group__cover">
|
||||||
<div className={styles.downloadCoverBackdrop}>
|
<div className="download-group__cover-backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
className={styles.downloadCoverImage}
|
className={styles.downloadCoverImage}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.downloadCoverContent}>
|
<div className="download-group__cover-content">
|
||||||
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.downloadRightContent}>
|
<div className="download-group__right-content">
|
||||||
<div className={styles.downloadDetails}>
|
<div className="download-group__details">
|
||||||
<div className={styles.downloadTitleWrapper}>
|
<div className="download-group__title-wrapper">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.downloadTitle}
|
className="download-group__title"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
buildGameDetailsPath({
|
buildGameDetailsPath({
|
||||||
|
@ -4,13 +4,15 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
|||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||||
import * as styles from "./downloads.css";
|
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { LibraryGame, SeedingStatus } from "@types";
|
import type { LibraryGame, SeedingStatus } from "@types";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import "./downloads.scss";
|
||||||
|
|
||||||
export default function Downloads() {
|
export default function Downloads() {
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
|
||||||
@ -121,8 +123,8 @@ export default function Downloads() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{hasItemsInLibrary ? (
|
{hasItemsInLibrary ? (
|
||||||
<section className={styles.downloadsContainer}>
|
<section className="downloads__container">
|
||||||
<div className={styles.downloadGroups}>
|
<div className="downloads__groups">
|
||||||
{downloadGroups.map((group) => (
|
{downloadGroups.map((group) => (
|
||||||
<DownloadGroup
|
<DownloadGroup
|
||||||
key={group.title}
|
key={group.title}
|
||||||
@ -136,8 +138,8 @@ export default function Downloads() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.noDownloads}>
|
<div className="downloads__no-downloads">
|
||||||
<div className={styles.arrowIcon}>
|
<div className="downloads__arrow-icon">
|
||||||
<ArrowDownIcon size={24} />
|
<ArrowDownIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_downloads_title")}</h2>
|
<h2>{t("no_downloads_title")}</h2>
|
||||||
|
@ -14,18 +14,5 @@
|
|||||||
.editor-header-title {
|
.editor-header-title {
|
||||||
font-size: 7px;
|
font-size: 7px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: globals.$body-color;
|
color: $body-color;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: globals.$spacing-unit;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-header-status {
|
|
||||||
width: globals.$spacing-unit;
|
|
||||||
height: globals.$spacing-unit;
|
|
||||||
background-color: globals.$body-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-flex;
|
|
||||||
align-self: center;
|
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,18 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import "../../scss/_variables.scss";
|
import { vars } from "@renderer/theme.css";
|
||||||
import "./editor.scss";
|
import "./editor.scss";
|
||||||
import { Editor as Monaco } from "@monaco-editor/react";
|
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const [code, setCode] = useState("");
|
|
||||||
const [currentCode, setCurrentCode] = useState("");
|
|
||||||
const [updated, setUpdated] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("spectre");
|
console.log("spectre");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUpdated(currentCode === code);
|
|
||||||
}, [code, currentCode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.ctrlKey && e.key === "s") {
|
|
||||||
e.preventDefault();
|
|
||||||
setCode(currentCode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [currentCode]);
|
|
||||||
|
|
||||||
const handleEditorChange = (value: string | undefined) => {
|
|
||||||
setCurrentCode(value || "");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<div className="editor-header">
|
<div className="editor-header">
|
||||||
<div className="editor-header-title">
|
<div className="editor-header-title">
|
||||||
<h1>CSS Editor</h1>
|
<h1>CSS Editor</h1>
|
||||||
{!updated && <div className="editor-header-status" />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -53,16 +27,7 @@ export default function Editor() {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Monaco
|
<p>spectre</p>
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
defaultLanguage="css"
|
|
||||||
theme="vs-dark"
|
|
||||||
value={currentCode}
|
|
||||||
onChange={handleEditorChange}
|
|
||||||
defaultValue={code}
|
|
||||||
className="editor-monaco"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,6 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./cloud-sync-files-modal.css";
|
|
||||||
import { formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
import { useToast } from "@renderer/hooks";
|
import { useToast } from "@renderer/hooks";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -99,7 +98,7 @@ export function CloudSyncFilesModal({
|
|||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
||||||
|
|
||||||
<div className={styles.mappingMethods}>
|
<div className="clound-sync-files-modal__mapping-methods">
|
||||||
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
||||||
<Button
|
<Button
|
||||||
key={mappingMethod}
|
key={mappingMethod}
|
||||||
@ -142,11 +141,11 @@ export function CloudSyncFilesModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ul className={styles.fileList}>
|
<ul className="cloud-sync-files-modal__files-list">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<li key={file.path} style={{ display: "flex" }}>
|
<li key={file.path} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
className={styles.fileItem}
|
className="cloud-sync-files-modal__file-item"
|
||||||
onClick={() => window.electron.showItemInFolder(file.path)}
|
onClick={() => window.electron.showItemInFolder(file.path)}
|
||||||
>
|
>
|
||||||
{file.path.split("/").at(-1)}
|
{file.path.split("/").at(-1)}
|
||||||
|
@ -2,7 +2,6 @@ import { Button, Modal, ModalProps } from "@renderer/components";
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
import * as styles from "./cloud-sync-modal.css";
|
|
||||||
import { formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
@ -18,7 +17,9 @@ import { useAppSelector, useToast } from "@renderer/hooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AxiosProgressEvent } from "axios";
|
import { AxiosProgressEvent } from "axios";
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
|
import "./cloud-sync-modal.scss"
|
||||||
|
import "../../../scss/_variables.scss"
|
||||||
|
|
||||||
export interface CloudSyncModalProps
|
export interface CloudSyncModalProps
|
||||||
extends Omit<ModalProps, "children" | "title"> {}
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
@ -95,7 +96,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (uploadingBackup) {
|
if (uploadingBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("uploading_backup")}
|
{t("uploading_backup")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -104,7 +105,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (restoringBackup) {
|
if (restoringBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("restoring_backup", {
|
{t("restoring_backup", {
|
||||||
progress: formatDownloadProgress(
|
progress: formatDownloadProgress(
|
||||||
backupDownloadProgress?.progress ?? 0
|
backupDownloadProgress?.progress ?? 0
|
||||||
@ -117,7 +118,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (loadingPreview) {
|
if (loadingPreview) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("loading_save_preview")}
|
{t("loading_save_preview")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -171,7 +172,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.manageFilesButton}
|
className="clound-sync-modal__manage-files-button"
|
||||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||||
disabled={disableActions}
|
disabled={disableActions}
|
||||||
>
|
>
|
||||||
@ -199,7 +200,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: SPACING_UNIT,
|
gap: "var(--spacing-unit)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2>{t("backups")}</h2>
|
<h2>{t("backups")}</h2>
|
||||||
@ -210,9 +211,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{artifacts.length > 0 ? (
|
{artifacts.length > 0 ? (
|
||||||
<ul className={styles.artifacts}>
|
<ul className="clound-sync-modal__artifacts">
|
||||||
{artifacts.map((artifact) => (
|
{artifacts.map((artifact) => (
|
||||||
<li key={artifact.id} className={styles.artifactButton}>
|
<li key={artifact.id} className="cloud-sync-modal__artifact-button">
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -93,6 +93,7 @@
|
|||||||
color: $muted-color;
|
color: $muted-color;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
@ -118,9 +119,5 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,22 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./gallery-slider.css";
|
import "./gallery-slider.scss"
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
|
const getButtonClasses = (visible: boolean, direction: "left" | "right") => {
|
||||||
|
return classNames("gallery-slider__button", {
|
||||||
|
"gallery-slider__button--visible": visible,
|
||||||
|
[`gallery-slider__button--${direction}`]: direction,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewClasses = (isActive: boolean) => {
|
||||||
|
return classNames("gallery-slider__media-preview-button", {
|
||||||
|
"gallery-slider__media-preview-button--active": isActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function GallerySlider() {
|
export function GallerySlider() {
|
||||||
const { shopDetails } = useContext(gameDetailsContext);
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
@ -97,11 +110,11 @@ export function GallerySlider() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasScreenshots && (
|
{hasScreenshots && (
|
||||||
<div className={styles.gallerySliderContainer}>
|
<div className="gallery-slider__container">
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setShowArrows(true)}
|
onMouseEnter={() => setShowArrows(true)}
|
||||||
onMouseLeave={() => setShowArrows(false)}
|
onMouseLeave={() => setShowArrows(false)}
|
||||||
className={styles.gallerySliderAnimationContainer}
|
className="gallery-slider__animation-container"
|
||||||
ref={mediaContainerRef}
|
ref={mediaContainerRef}
|
||||||
>
|
>
|
||||||
{shopDetails.movies &&
|
{shopDetails.movies &&
|
||||||
@ -109,7 +122,7 @@ export function GallerySlider() {
|
|||||||
<video
|
<video
|
||||||
key={video.id}
|
key={video.id}
|
||||||
controls
|
controls
|
||||||
className={styles.gallerySliderMedia}
|
className="gallery-slider__media"
|
||||||
poster={video.thumbnail}
|
poster={video.thumbnail}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
loop
|
loop
|
||||||
@ -124,7 +137,7 @@ export function GallerySlider() {
|
|||||||
shopDetails.screenshots?.map((image, i) => (
|
shopDetails.screenshots?.map((image, i) => (
|
||||||
<img
|
<img
|
||||||
key={image.id}
|
key={image.id}
|
||||||
className={styles.gallerySliderMedia}
|
className="gallery-slider__media"
|
||||||
src={image.path_full}
|
src={image.path_full}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
@ -135,10 +148,7 @@ export function GallerySlider() {
|
|||||||
<button
|
<button
|
||||||
onClick={showPrevImage}
|
onClick={showPrevImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton({
|
className={getButtonClasses(showArrows, "left")}
|
||||||
visible: showArrows,
|
|
||||||
direction: "left",
|
|
||||||
})}
|
|
||||||
aria-label={t("previous_screenshot")}
|
aria-label={t("previous_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@ -148,10 +158,7 @@ export function GallerySlider() {
|
|||||||
<button
|
<button
|
||||||
onClick={showNextImage}
|
onClick={showNextImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton({
|
className={getButtonClasses(showArrows, "right")}
|
||||||
visible: showArrows,
|
|
||||||
direction: "right",
|
|
||||||
})}
|
|
||||||
aria-label={t("next_screenshot")}
|
aria-label={t("next_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@ -159,20 +166,18 @@ export function GallerySlider() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
|
<div className="gallery-slider__preview" ref={scrollContainerRef}>
|
||||||
{previews.map((media, i) => (
|
{previews.map((media, i) => (
|
||||||
<button
|
<button
|
||||||
key={media.id}
|
key={media.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.mediaPreviewButton({
|
className={getPreviewClasses(mediaIndex === i)}
|
||||||
active: mediaIndex === i,
|
|
||||||
})}
|
|
||||||
onClick={() => setMediaIndex(i)}
|
onClick={() => setMediaIndex(i)}
|
||||||
aria-label={t("open_screenshot", { number: i + 1 })}
|
aria-label={t("open_screenshot", { number: i + 1 })}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={media.thumbnail}
|
src={media.thumbnail}
|
||||||
className={styles.mediaPreview}
|
className="gallery-slider__media-preview"
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@ import { DescriptionHeader } from "./description-header/description-header";
|
|||||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||||
import { Sidebar } from "./sidebar/sidebar";
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import "./game-details.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { AuthPage, steamUrlBuilder } from "@shared";
|
import { AuthPage, steamUrlBuilder } from "@shared";
|
||||||
@ -118,10 +118,10 @@ export function GameDetailsContent() {
|
|||||||
}, [getGameArtifacts]);
|
}, [getGameArtifacts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
<div className="game-details__blurred-content">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
src={steamUrlBuilder.libraryHero(objectId!)}
|
||||||
className={styles.heroImage}
|
className="game-details__hero-image"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
@ -129,9 +129,9 @@ export function GameDetailsContent() {
|
|||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={styles.container}
|
className="game-details__container"
|
||||||
>
|
>
|
||||||
<div ref={heroRef} className={styles.hero}>
|
<div ref={heroRef} className="game-details__hero">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: gameColor,
|
backgroundColor: gameColor,
|
||||||
@ -141,19 +141,19 @@ export function GameDetailsContent() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.heroLogoBackdrop}
|
className="game-details__hero-logo-backdrop"
|
||||||
style={{ opacity: backdropOpactiy }}
|
style={{ opacity: backdropOpactiy }}
|
||||||
>
|
>
|
||||||
<div className={styles.heroContent}>
|
<div className="game-details__hero-content">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId!)}
|
src={steamUrlBuilder.logo(objectId!)}
|
||||||
className={styles.gameLogo}
|
className="game-details__game-logo"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.cloudSyncButton}
|
className="game-details__cloud-sync-button"
|
||||||
onClick={handleCloudSaveButtonClick}
|
onClick={handleCloudSaveButtonClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -180,8 +180,8 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
||||||
|
|
||||||
<div className={styles.descriptionContainer}>
|
<div className="game-details__description-container">
|
||||||
<div className={styles.descriptionContent}>
|
<div className="game-details__description-content">
|
||||||
<DescriptionHeader />
|
<DescriptionHeader />
|
||||||
<GallerySlider />
|
<GallerySlider />
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ export function GameDetailsContent() {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: aboutTheGame,
|
__html: aboutTheGame,
|
||||||
}}
|
}}
|
||||||
className={styles.description}
|
className="game-details__description"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@ import Skeleton from "react-loading-skeleton";
|
|||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import "./game-details.scss";
|
||||||
import * as sidebarStyles from "./sidebar/sidebar.css";
|
|
||||||
import * as descriptionHeaderStyles from "./description-header/description-header.css";
|
import "./sidebar/sidebar.scss";
|
||||||
|
|
||||||
|
import "./description-header/description-header.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -12,54 +14,54 @@ export function GameDetailsSkeleton() {
|
|||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="game-details_container">
|
||||||
<div className={styles.hero}>
|
<div className="game-details_hero">
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.heroPanelSkeleton}>
|
<div className="game-details__hero-panel-skeleton">
|
||||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
<section className="description-header">
|
||||||
<Skeleton width={155} />
|
<Skeleton width={155} />
|
||||||
<Skeleton width={135} />
|
<Skeleton width={135} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className="game-details__description-container">
|
||||||
<div className={styles.descriptionContent}>
|
<div className="game-details__description-content">
|
||||||
<div className={descriptionHeaderStyles.descriptionHeader}>
|
<div className="description-header">
|
||||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
<section className="description-header__info">
|
||||||
<Skeleton width={145} />
|
<Skeleton width={145} />
|
||||||
<Skeleton width={150} />
|
<Skeleton width={150} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionSkeleton}>
|
<div className="game-details__description-skeleton">
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<Skeleton key={index} />
|
<Skeleton key={index} />
|
||||||
))}
|
))}
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
{Array.from({ length: 2 }).map((_, index) => (
|
{Array.from({ length: 2 }).map((_, index) => (
|
||||||
<Skeleton key={index} />
|
<Skeleton key={index} />
|
||||||
))}
|
))}
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={sidebarStyles.contentSidebar}>
|
<div className="sidebar__content-sidebar">
|
||||||
<div className={sidebarStyles.requirementButtonContainer}>
|
<div className="sidebar__requirements-button-container">
|
||||||
<Button
|
<Button
|
||||||
className={sidebarStyles.requirementButton}
|
className="sidebar__requirement-button"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("minimum")}
|
{t("minimum")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={sidebarStyles.requirementButton}
|
className="sidebar__requirement-button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("recommended")}
|
{t("recommended")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={sidebarStyles.requirementsDetailsSkeleton}>
|
<div className="sidebar__requirements-details-skeleton">
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
<Skeleton key={index} height={20} />
|
<Skeleton key={index} height={20} />
|
||||||
))}
|
))}
|
||||||
|
@ -22,8 +22,9 @@ $hero-height: 300px;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
|
|
||||||
&--blurred-content {
|
&__blurred-content {
|
||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,9 +11,10 @@ import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||||
import * as styles from "./game-details.css";
|
|
||||||
|
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
import "./game-details.scss";
|
||||||
|
|
||||||
|
|
||||||
import { GameDetailsContent } from "./game-details-content";
|
import { GameDetailsContent } from "./game-details-content";
|
||||||
import {
|
import {
|
||||||
@ -149,7 +150,7 @@ export default function GameDetails() {
|
|||||||
</CloudSyncContextConsumer>
|
</CloudSyncContextConsumer>
|
||||||
|
|
||||||
<SkeletonTheme
|
<SkeletonTheme
|
||||||
baseColor={vars.color.background}
|
baseColor="#1c1c1c"
|
||||||
highlightColor="#444"
|
highlightColor="#444"
|
||||||
>
|
>
|
||||||
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
||||||
@ -185,7 +186,7 @@ export default function GameDetails() {
|
|||||||
|
|
||||||
{fromRandomizer && (
|
{fromRandomizer && (
|
||||||
<Button
|
<Button
|
||||||
className={styles.randomizerButton}
|
className="game-details__randomizer-button"
|
||||||
onClick={handleRandomizerClick}
|
onClick={handleRandomizerClick}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={!randomGame || randomizerLocked}
|
disabled={!randomGame || randomizerLocked}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.hero-panel-actions {
|
.hero-panel-actions {
|
||||||
&__action {
|
&__action {
|
||||||
|
@ -8,7 +8,7 @@ import { Button } from "@renderer/components";
|
|||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./hero-panel-actions.css";
|
import "./hero-panel-actions.scss"
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ export function HeroPanelActions() {
|
|||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={toggleLibraryGameDisabled}
|
disabled={toggleLibraryGameDisabled}
|
||||||
onClick={addGameToLibrary}
|
onClick={addGameToLibrary}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
<PlusCircleIcon />
|
<PlusCircleIcon />
|
||||||
{t("add_to_library")}
|
{t("add_to_library")}
|
||||||
@ -96,7 +96,7 @@ export function HeroPanelActions() {
|
|||||||
onClick={() => setShowRepacksModal(true)}
|
onClick={() => setShowRepacksModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
{t("open_download_options")}
|
{t("open_download_options")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -109,7 +109,7 @@ export function HeroPanelActions() {
|
|||||||
onClick={closeGame}
|
onClick={closeGame}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
{t("close")}
|
{t("close")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -122,7 +122,7 @@ export function HeroPanelActions() {
|
|||||||
onClick={openGame}
|
onClick={openGame}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting || isGameRunning}
|
disabled={deleting || isGameRunning}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
{t("play")}
|
{t("play")}
|
||||||
@ -135,7 +135,7 @@ export function HeroPanelActions() {
|
|||||||
onClick={() => setShowRepacksModal(true)}
|
onClick={() => setShowRepacksModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={isGameDownloading || !repacks.length}
|
disabled={isGameDownloading || !repacks.length}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{t("download")}
|
{t("download")}
|
||||||
@ -154,16 +154,16 @@ export function HeroPanelActions() {
|
|||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.actions}>
|
<div className="hero-panel-actions__actions">
|
||||||
{gameActionButton()}
|
{gameActionButton()}
|
||||||
|
|
||||||
<div className={styles.separator} />
|
<div className="hero-panel-actions__separator" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowGameOptionsModal(true)}
|
onClick={() => setShowGameOptionsModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className={styles.heroPanelAction}
|
className="hero-panel-actions__action"
|
||||||
>
|
>
|
||||||
<GearIcon />
|
<GearIcon />
|
||||||
{t("options")}
|
{t("options")}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./hero-panel.css";
|
import "./hero-panel.scss"
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
@ -55,8 +55,8 @@ export function HeroPanelPlaytime() {
|
|||||||
game.status === "active" && lastPacket?.game.id === game.id;
|
game.status === "active" && lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
const downloadInProgressInfo = (
|
const downloadInProgressInfo = (
|
||||||
<div className={styles.downloadDetailsRow}>
|
<div className="hero-panel__download-details-row">
|
||||||
<Link to="/downloads" className={styles.downloadsLink}>
|
<Link to="/downloads" className="hero-panel__downloads-link">
|
||||||
{game.status === "active"
|
{game.status === "active"
|
||||||
? t("download_in_progress")
|
? t("download_in_progress")
|
||||||
: t("download_paused")}
|
: t("download_paused")}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.hero-panel {
|
.hero-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useDate, useDownload } from "@renderer/hooks";
|
import { useDate, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import { HeroPanelActions } from "./hero-panel-actions";
|
import { HeroPanelActions } from "./hero-panel-actions";
|
||||||
import * as styles from "./hero-panel.css";
|
import "./hero-panel.scss"
|
||||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
@ -13,6 +13,18 @@ export interface HeroPanelProps {
|
|||||||
isHeaderStuck: boolean;
|
isHeaderStuck: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPanelClasses = (stuck: boolean) => {
|
||||||
|
return classNames("hero-panel", {
|
||||||
|
"hero-panel--stuck": stuck,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressBarClasses = (disabled: boolean) => {
|
||||||
|
return classNames("hero-panel__progress-bar", {
|
||||||
|
"hero-panel__progress-bar--disabled": disabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
@ -57,10 +69,10 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{ backgroundColor: gameColor }}
|
style={{ backgroundColor: gameColor }}
|
||||||
className={styles.panel({ stuck: isHeaderStuck })}
|
className={getPanelClasses(isHeaderStuck)}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>{getInfo()}</div>
|
<div className="hero-panel__content">{getInfo()}</div>
|
||||||
<div className={styles.actions}>
|
<div className="hero-panel__actions">
|
||||||
<HeroPanelActions />
|
<HeroPanelActions />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -70,12 +82,10 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|||||||
value={
|
value={
|
||||||
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
||||||
}
|
}
|
||||||
className={styles.progressBar({
|
className={getProgressBarClasses(game?.status === "paused")}
|
||||||
disabled: game?.status === "paused",
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,15 +1,15 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./download-settings-modal.css";
|
|
||||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||||
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||||
|
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
|
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
|
||||||
|
import "./download-settings-modal.scss";
|
||||||
|
import "../../../scss/_variables.scss"
|
||||||
|
|
||||||
export interface DownloadSettingsModalProps {
|
export interface DownloadSettingsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -145,21 +145,21 @@ export function DownloadSettingsModal({
|
|||||||
})}
|
})}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className="download-settings-modal__container">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: "var(--spacing-unit)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{t("downloader")}</span>
|
<span>{t("downloader")}</span>
|
||||||
|
|
||||||
<div className={styles.downloaders}>
|
<div className="download-settings-modal__downloaders">
|
||||||
{downloaders.map((downloader) => (
|
{downloaders.map((downloader) => (
|
||||||
<Button
|
<Button
|
||||||
key={downloader}
|
key={downloader}
|
||||||
className={styles.downloaderOption}
|
className="download-settings-modal__downloader-option"
|
||||||
theme={
|
theme={
|
||||||
selectedDownloader === downloader ? "primary" : "outline"
|
selectedDownloader === downloader ? "primary" : "outline"
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ export function DownloadSettingsModal({
|
|||||||
onClick={() => setSelectedDownloader(downloader)}
|
onClick={() => setSelectedDownloader(downloader)}
|
||||||
>
|
>
|
||||||
{selectedDownloader === downloader && (
|
{selectedDownloader === downloader && (
|
||||||
<CheckCircleFillIcon className={styles.downloaderIcon} />
|
<CheckCircleFillIcon className="download-settings-modal__downloader-icon" />
|
||||||
)}
|
)}
|
||||||
{DOWNLOADER_NAME[downloader]}
|
{DOWNLOADER_NAME[downloader]}
|
||||||
</Button>
|
</Button>
|
||||||
@ -182,7 +182,7 @@ export function DownloadSettingsModal({
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: "var(--spacing-unit)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
@ -193,7 +193,7 @@ export function DownloadSettingsModal({
|
|||||||
error={
|
error={
|
||||||
hasWritePermission === false ? (
|
hasWritePermission === false ? (
|
||||||
<span
|
<span
|
||||||
className={styles.pathError}
|
className="download-settings-modal__path-error"
|
||||||
data-open-article="cannot-write-directory"
|
data-open-article="cannot-write-directory"
|
||||||
>
|
>
|
||||||
{t("no_write_permission")}
|
{t("no_write_permission")}
|
||||||
@ -212,7 +212,7 @@ export function DownloadSettingsModal({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className={styles.hintText}>
|
<p className="download-settings-modal__hint-text">
|
||||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||||
<Link to="/settings" />
|
<Link to="/settings" />
|
||||||
</Trans>
|
</Trans>
|
||||||
|
@ -2,7 +2,6 @@ import { useContext, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
import * as styles from "./game-options-modal.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
@ -10,6 +9,7 @@ import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
|||||||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
|
import "./game-options-modal.scss";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -199,10 +199,10 @@ export function GameOptionsModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
large={true}
|
large={true}
|
||||||
>
|
>
|
||||||
<div className={styles.optionsContainer}>
|
<div className="game-options-modal__options-container">
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__game-option-header">
|
||||||
<h2>{t("executable_section_title")}</h2>
|
<h2>{t("executable_section_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__game-option-header-description">
|
||||||
{t("executable_section_description")}
|
{t("executable_section_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -233,7 +233,7 @@ export function GameOptionsModal({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{game.executablePath && (
|
{game.executablePath && (
|
||||||
<div className={styles.gameOptionRow}>
|
<div className="game-options-modal__game-option-row">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
@ -248,10 +248,10 @@ export function GameOptionsModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowWinePrefixConfiguration && (
|
{shouldShowWinePrefixConfiguration && (
|
||||||
<div className={styles.optionsContainer}>
|
<div className="game-options-modal__options-container">
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__game-option-header">
|
||||||
<h2>{t("wine_prefix")}</h2>
|
<h2>{t("wine_prefix")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__game-option-header-description">
|
||||||
{t("wine_prefix_description")}
|
{t("wine_prefix_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -286,9 +286,9 @@ export function GameOptionsModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowLaunchOptionsConfiguration && (
|
{shouldShowLaunchOptionsConfiguration && (
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__game-option-header">
|
||||||
<h2>{t("launch_options")}</h2>
|
<h2>{t("launch_options")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__game-option-description">
|
||||||
{t("launch_options_description")}
|
{t("launch_options_description")}
|
||||||
</h4>
|
</h4>
|
||||||
<TextField
|
<TextField
|
||||||
@ -307,14 +307,14 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__game-option-header">
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__game-option-description">
|
||||||
{t("downloads_section_description")}
|
{t("downloads_section_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<div className="game-options-modal__game-option-row">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowRepacksModal(true)}
|
onClick={() => setShowRepacksModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
@ -333,14 +333,14 @@ export function GameOptionsModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className="game-options-modal__game-option-header">
|
||||||
<h2>{t("danger_zone_section_title")}</h2>
|
<h2>{t("danger_zone_section_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className="game-options-modal__game-option-description">
|
||||||
{t("danger_zone_section_description")}
|
{t("danger_zone_section_description")}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gameOptionRow}>
|
<div className="game-options-modal__game-option-row">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowRemoveGameModal(true)}
|
onClick={() => setShowRemoveGameModal(true)}
|
||||||
theme="danger"
|
theme="danger"
|
||||||
|
@ -30,7 +30,7 @@ export function RemoveGameFromLibraryModal({
|
|||||||
description={t("remove_from_library_description", { game: game.title })}
|
description={t("remove_from_library_description", { game: game.title })}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.deleteActionsButtonsCtn}>
|
<div className="remove-from-library-modal__delete-actions-buttons-ctn">
|
||||||
<Button onClick={handleRemoveGame} theme="outline">
|
<Button onClick={handleRemoveGame} theme="outline">
|
||||||
{t("remove")}
|
{t("remove")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
|
|
||||||
import * as styles from "./repacks-modal.css";
|
|
||||||
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
|
|
||||||
|
import "./repacks-modal.scss";
|
||||||
|
import "../../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface RepacksModalProps {
|
export interface RepacksModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
startDownload: (
|
startDownload: (
|
||||||
@ -86,11 +86,11 @@ export function RepacksModal({
|
|||||||
description={t("repacks_modal_description")}
|
description={t("repacks_modal_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
|
<div className="repacks-modal__filter">
|
||||||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.repacks}>
|
<div className="repacks-modal__repacks">
|
||||||
{filteredRepacks.map((repack) => {
|
{filteredRepacks.map((repack) => {
|
||||||
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ export function RepacksModal({
|
|||||||
key={repack.id}
|
key={repack.id}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
onClick={() => handleRepackClick(repack)}
|
onClick={() => handleRepackClick(repack)}
|
||||||
className={styles.repackButton}
|
className="repacks-modal__button"
|
||||||
>
|
>
|
||||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||||
{repack.title}
|
{repack.title}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import * as styles from "./remove-from-library-modal.css";
|
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
|
import "./remove-from-library-modal.scss"
|
||||||
type ResetAchievementsModalProps = Readonly<{
|
type ResetAchievementsModalProps = Readonly<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
game: Game;
|
game: Game;
|
||||||
@ -34,7 +34,7 @@ export function ResetAchievementsModal({
|
|||||||
game: game.title,
|
game: game.title,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.deleteActionsButtonsCtn}>
|
<div className="remove-from-library-modal__delete-actions-buttons-ctn">
|
||||||
<Button onClick={handleResetAchievements} theme="outline">
|
<Button onClick={handleResetAchievements} theme="outline">
|
||||||
{t("reset_achievements")}
|
{t("reset_achievements")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ChevronDownIcon } from "@primer/octicons-react";
|
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
import * as styles from "./sidebar-section.css";
|
import "./sidebar-section.scss";
|
||||||
|
|
||||||
export interface SidebarSectionProps {
|
export interface SidebarSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -17,9 +17,13 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={styles.sidebarSectionButton}
|
className="sidebar-section__button"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className={styles.chevron({ open: isOpen })} />
|
<ChevronDownIcon
|
||||||
|
className={classNames("chevron", {
|
||||||
|
"chevron--open": isOpen,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { HowLongToBeatCategory } from "@types";
|
import type { HowLongToBeatCategory } from "@types";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
|
||||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||||
|
|
||||||
|
import "./sidebar.scss"
|
||||||
|
import "../../../scss/_variables.scss"
|
||||||
|
|
||||||
const durationTranslation: Record<string, string> = {
|
const durationTranslation: Record<string, string> = {
|
||||||
Hours: "hours",
|
Hours: "hours",
|
||||||
Mins: "minutes",
|
Mins: "minutes",
|
||||||
@ -30,17 +30,17 @@ export function HowLongToBeatSection({
|
|||||||
if (!howLongToBeatData && !isLoading) return null;
|
if (!howLongToBeatData && !isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||||
<SidebarSection title="HowLongToBeat">
|
<SidebarSection title="HowLongToBeat">
|
||||||
<ul className={styles.howLongToBeatCategoriesList}>
|
<ul className="sidebar__how-long-to-beat-categories-list">
|
||||||
{howLongToBeatData
|
{howLongToBeatData
|
||||||
? howLongToBeatData.map((category) => (
|
? howLongToBeatData.map((category) => (
|
||||||
<li
|
<li
|
||||||
key={category.title}
|
key={category.title}
|
||||||
className={styles.howLongToBeatCategory}
|
className="sidebar__how-long-to-beat-category"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={styles.howLongToBeatCategoryLabel}
|
className="sidebar__how-long-to-beat-category"
|
||||||
style={{
|
style={{
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
@ -48,7 +48,7 @@ export function HowLongToBeatSection({
|
|||||||
{category.title}
|
{category.title}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className={styles.howLongToBeatCategoryLabel}>
|
<p className="sidebar__how-long-to-beat-category">
|
||||||
{getDuration(category.duration)}
|
{getDuration(category.duration)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export function HowLongToBeatSection({
|
|||||||
: Array.from({ length: 4 }).map((_, index) => (
|
: Array.from({ length: 4 }).map((_, index) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={index}
|
key={index}
|
||||||
className={styles.howLongToBeatCategorySkeleton}
|
className="sidebar__how-long-to-beat-category-skeleton"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -7,7 +7,6 @@ import type {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||||
import {
|
import {
|
||||||
@ -20,8 +19,9 @@ import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
|||||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
|
import "./sidebar.scss";
|
||||||
|
import "../../../scss/_variables.scss";
|
||||||
|
|
||||||
const fakeAchievements: UserAchievement[] = [
|
const fakeAchievements: UserAchievement[] = [
|
||||||
{
|
{
|
||||||
@ -117,171 +117,158 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
}, [objectId, shop, gameTitle]);
|
}, [objectId, shop, gameTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className="sidebar__content">
|
||||||
{userDetails === null && (
|
{userDetails === null && (
|
||||||
<SidebarSection title={t("achievements")}>
|
<SidebarSection title={t("achievements")}>
|
||||||
<div
|
<div className="sidebar__overlay">
|
||||||
style={{
|
<LockIcon size={36} />
|
||||||
position: "absolute",
|
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||||
zIndex: 1,
|
|
||||||
inset: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "rgba(0, 0, 0, 0.7)",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LockIcon size={36} />
|
|
||||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
|
||||||
</div>
|
|
||||||
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
|
|
||||||
{fakeAchievements.map((achievement, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<div className={styles.listItem}>
|
|
||||||
<img
|
|
||||||
style={{ filter: "blur(8px)" }}
|
|
||||||
className={styles.listItemImage({
|
|
||||||
unlocked: achievement.unlocked,
|
|
||||||
})}
|
|
||||||
src={achievement.icon}
|
|
||||||
alt={achievement.displayName}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{achievement.displayName}</p>
|
|
||||||
<small>
|
|
||||||
{achievement.unlockTime != null &&
|
|
||||||
formatDateTime(achievement.unlockTime)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</SidebarSection>
|
|
||||||
)}
|
|
||||||
{userDetails && achievements && achievements.length > 0 && (
|
|
||||||
<SidebarSection
|
|
||||||
title={t("achievements_count", {
|
|
||||||
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
|
||||||
achievementsCount: achievements.length,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ul className={styles.list}>
|
|
||||||
{!hasActiveSubscription && (
|
|
||||||
<button
|
|
||||||
className={styles.subscriptionRequiredButton}
|
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
|
||||||
>
|
|
||||||
<CloudOfflineIcon size={16} />
|
|
||||||
<span>{t("achievements_not_sync")}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{achievements.slice(0, 4).map((achievement, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<Link
|
|
||||||
to={buildGameAchievementPath({
|
|
||||||
shop: shop,
|
|
||||||
objectId: objectId!,
|
|
||||||
title: gameTitle,
|
|
||||||
})}
|
|
||||||
className={styles.listItem}
|
|
||||||
title={achievement.description}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={styles.listItemImage({
|
|
||||||
unlocked: achievement.unlocked,
|
|
||||||
})}
|
|
||||||
src={achievement.icon}
|
|
||||||
alt={achievement.displayName}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{achievement.displayName}</p>
|
|
||||||
<small>
|
|
||||||
{achievement.unlockTime != null &&
|
|
||||||
formatDateTime(achievement.unlockTime)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
style={{ textAlign: "center" }}
|
|
||||||
to={buildGameAchievementPath({
|
|
||||||
shop: shop,
|
|
||||||
objectId: objectId!,
|
|
||||||
title: gameTitle,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t("see_all_achievements")}
|
|
||||||
</Link>
|
|
||||||
</ul>
|
|
||||||
</SidebarSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{stats && (
|
|
||||||
<SidebarSection title={t("stats")}>
|
|
||||||
<div className={styles.statsSection}>
|
|
||||||
<div className={styles.statsCategory}>
|
|
||||||
<p className={styles.statsCategoryTitle}>
|
|
||||||
<DownloadIcon size={18} />
|
|
||||||
{t("download_count")}
|
|
||||||
</p>
|
|
||||||
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.statsCategory}>
|
|
||||||
<p className={styles.statsCategoryTitle}>
|
|
||||||
<PeopleIcon size={18} />
|
|
||||||
{t("player_count")}
|
|
||||||
</p>
|
|
||||||
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HowLongToBeatSection
|
|
||||||
howLongToBeatData={howLongToBeat.data}
|
|
||||||
isLoading={howLongToBeat.isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarSection title={t("requirements")}>
|
|
||||||
<div className={styles.requirementButtonContainer}>
|
|
||||||
<Button
|
|
||||||
className={styles.requirementButton}
|
|
||||||
onClick={() => setActiveRequirement("minimum")}
|
|
||||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
|
||||||
>
|
|
||||||
{t("minimum")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className={styles.requirementButton}
|
|
||||||
onClick={() => setActiveRequirement("recommended")}
|
|
||||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
|
||||||
>
|
|
||||||
{t("recommended")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ul className="sidebar__list sidebar__list--blurred">
|
||||||
<div
|
{fakeAchievements.map((achievement, index) => (
|
||||||
className={styles.requirementsDetails}
|
<li key={index}>
|
||||||
dangerouslySetInnerHTML={{
|
<div className="sidebar__list-item">
|
||||||
__html:
|
<img
|
||||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
className={classNames("sidebar__list-item-image", {
|
||||||
t(`no_${activeRequirement}_requirements`, {
|
"sidebar__list-item-image--unlocked": achievement.unlocked,
|
||||||
gameTitle,
|
"sidebar__list-item-image--blurred": true,
|
||||||
}),
|
})}
|
||||||
}}
|
src={achievement.icon}
|
||||||
/>
|
alt={achievement.displayName}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{achievement.displayName}</p>
|
||||||
|
<small>
|
||||||
|
{achievement.unlockTime != null &&
|
||||||
|
formatDateTime(achievement.unlockTime)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</aside>
|
)}
|
||||||
);
|
{userDetails && achievements && achievements.length > 0 && (
|
||||||
|
<SidebarSection
|
||||||
|
title={t("achievements_count", {
|
||||||
|
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||||
|
achievementsCount: achievements.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ul className="sidebar__list">
|
||||||
|
{!hasActiveSubscription && (
|
||||||
|
<button
|
||||||
|
className="sidebar__subscription-required-button"
|
||||||
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
|
>
|
||||||
|
<CloudOfflineIcon size={16} />
|
||||||
|
<span>{t("achievements_not_sync")}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{achievements.slice(0, 4).map((achievement, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<Link
|
||||||
|
to={buildGameAchievementPath({
|
||||||
|
shop: shop,
|
||||||
|
objectId: objectId!,
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
|
className="sidebar__list-item"
|
||||||
|
title={achievement.description}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={classNames("achievements__list-item-image", {
|
||||||
|
"achievements__list-item-image--unlocked":
|
||||||
|
achievement.unlocked,
|
||||||
|
})}
|
||||||
|
src={achievement.icon}
|
||||||
|
alt={achievement.displayName}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{achievement.displayName}</p>
|
||||||
|
<small>
|
||||||
|
{achievement.unlockTime != null &&
|
||||||
|
formatDateTime(achievement.unlockTime)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
to={buildGameAchievementPath({
|
||||||
|
shop: shop,
|
||||||
|
objectId: objectId!,
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t("see_all_achievements")}
|
||||||
|
</Link>
|
||||||
|
</ul>
|
||||||
|
</SidebarSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stats && (
|
||||||
|
<SidebarSection title={t("stats")}>
|
||||||
|
<div className="sidebar__stats-section">
|
||||||
|
<div className="sidebar__stats-category">
|
||||||
|
<p className="sidebar__stats-category-title">
|
||||||
|
<DownloadIcon size={18} />
|
||||||
|
{t("download_count")}
|
||||||
|
</p>
|
||||||
|
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sidebar__stats-category">
|
||||||
|
<p className="sidebar__stats-category-title">
|
||||||
|
<PeopleIcon size={18} />
|
||||||
|
{t("player_count")}
|
||||||
|
</p>
|
||||||
|
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HowLongToBeatSection
|
||||||
|
howLongToBeatData={howLongToBeat.data}
|
||||||
|
isLoading={howLongToBeat.isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarSection title={t("requirements")}>
|
||||||
|
<div className="sidebar__requirement-button-container">
|
||||||
|
<Button
|
||||||
|
className="sidebar__requirement-button"
|
||||||
|
onClick={() => setActiveRequirement("minimum")}
|
||||||
|
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||||
|
>
|
||||||
|
{t("minimum")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="sidebar__requirement-button"
|
||||||
|
onClick={() => setActiveRequirement("recommended")}
|
||||||
|
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||||
|
>
|
||||||
|
{t("recommended")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="sidebar__requirements-details"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||||
|
t(`no_${activeRequirement}_requirements`, {
|
||||||
|
gameTitle,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SidebarSection>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ import flameIconStatic from "@renderer/assets/icons/flame-static.png";
|
|||||||
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
|
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
|
||||||
import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
||||||
|
|
||||||
import * as styles from "./home.css";
|
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
import "./home.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
@ -94,14 +94,14 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||||
<section className={styles.content}>
|
<section className="home__content">
|
||||||
<h2>{t("featured")}</h2>
|
<h2>{t("featured")}</h2>
|
||||||
|
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
<section className={styles.homeHeader}>
|
<section className="home__header">
|
||||||
<ul className={styles.buttonsList}>
|
<ul className="home__buttons-list">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<li key={category}>
|
<li key={category}>
|
||||||
<Button
|
<Button
|
||||||
@ -121,13 +121,13 @@ export default function Home() {
|
|||||||
<img
|
<img
|
||||||
src={flameIconStatic}
|
src={flameIconStatic}
|
||||||
alt="Flame icon"
|
alt="Flame icon"
|
||||||
className={styles.flameIcon}
|
className="home__flame-icon"
|
||||||
style={{ display: animateFlame ? "none" : "block" }}
|
style={{ display: animateFlame ? "none" : "block" }}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={flameIconAnimated}
|
src={flameIconAnimated}
|
||||||
alt="Flame animation"
|
alt="Flame animation"
|
||||||
className={styles.flameIcon}
|
className="home__flame-icon"
|
||||||
style={{ display: animateFlame ? "block" : "none" }}
|
style={{ display: animateFlame ? "block" : "none" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -155,7 +155,7 @@ export default function Home() {
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
<h2 style={{ display: "flex", gap: 8 }}>
|
||||||
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
||||||
<div style={{ width: 24, height: 24, position: "relative" }}>
|
<div style={{ width: 24, height: 24, position: "relative" }}>
|
||||||
<img
|
<img
|
||||||
@ -174,10 +174,10 @@ export default function Home() {
|
|||||||
{t(currentCatalogueCategory)}
|
{t(currentCatalogueCategory)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<section className={styles.cards}>
|
<section className="home__cards">
|
||||||
{isLoading
|
{isLoading
|
||||||
? Array.from({ length: 12 }).map((_, index) => (
|
? Array.from({ length: 12 }).map((_, index) => (
|
||||||
<Skeleton key={index} className={styles.cardSkeleton} />
|
<Skeleton key={index} className="home__card-skeleton" />
|
||||||
))
|
))
|
||||||
: catalogue[currentCatalogueCategory].map((result) => (
|
: catalogue[currentCatalogueCategory].map((result) => (
|
||||||
<GameCard
|
<GameCard
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.edit-profile-modal {
|
.edit-profile-modal {
|
||||||
&__profile-avatar-edit-container {
|
&__profile-avatar-edit-container {
|
||||||
@ -31,4 +31,10 @@
|
|||||||
&__profile-avatar-edit-container:hover &__profile-avatar-edit-overlay {
|
&__profile-avatar-edit-container:hover &__profile-avatar-edit-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__profile-avatar-container {
|
||||||
|
gap: #{$spacing-unit * 3};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,14 @@ 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;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -87,13 +87,7 @@ export function EditProfileModal(
|
|||||||
width: "350px",
|
width: "350px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="edit-profile-modal__profile-avatar-container">
|
||||||
style={{
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="profileImageUrl"
|
name="profileImageUrl"
|
||||||
@ -140,7 +134,7 @@ export function EditProfileModal(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileAvatarEditContainer}
|
className="edit-profile-modal__profile-avatar-edit-container"
|
||||||
onClick={handleChangeProfileAvatar}
|
onClick={handleChangeProfileAvatar}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -149,7 +143,7 @@ export function EditProfileModal(
|
|||||||
alt={userDetails?.displayName}
|
alt={userDetails?.displayName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.profileAvatarEditOverlay}>
|
<div className="edit-profile-modal__profile-avatar-edit-overlay">
|
||||||
<DeviceCameraIcon size={38} />
|
<DeviceCameraIcon size={38} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -166,8 +160,7 @@ export function EditProfileModal(
|
|||||||
error={errors.displayName?.message}
|
error={errors.displayName?.message}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<small style={{ marginTop: `${8 * 2}px` }}>
|
||||||
<small style={{ marginTop: `${SPACING_UNIT * 2}px` }}>
|
|
||||||
<Trans i18nKey="privacy_hint" ns="user_profile">
|
<Trans i18nKey="privacy_hint" ns="user_profile">
|
||||||
<Link to="/settings" />
|
<Link to="/settings" />
|
||||||
</Trans>
|
</Trans>
|
||||||
@ -175,7 +168,7 @@ export function EditProfileModal(
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
style={{ alignSelf: "end", marginTop: `${SPACING_UNIT * 3}px` }}
|
style={{ alignSelf: "end", marginTop: `${8 * 3}px` }}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{isSubmitting ? t("saving") : t("save")}
|
{isSubmitting ? t("saving") : t("save")}
|
||||||
|
@ -3,8 +3,8 @@ 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 "./profile-content.scss";
|
||||||
|
|
||||||
export function FriendsBox() {
|
export function FriendsBox() {
|
||||||
const { userProfile, userStats } = useContext(userProfileContext);
|
const { userProfile, userStats } = useContext(userProfileContext);
|
||||||
@ -32,15 +32,15 @@ export function FriendsBox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="profile-content__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="profile-content__box">
|
||||||
<ul className={styles.list}>
|
<ul className="profile-content__list">
|
||||||
{userProfile?.friends.map((friend) => (
|
{userProfile?.friends.map((friend) => (
|
||||||
<li
|
<li
|
||||||
key={friend.id}
|
key={friend.id}
|
||||||
@ -50,7 +50,10 @@ export function FriendsBox() {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link to={`/profile/${friend.id}`} className={styles.listItem}>
|
<Link
|
||||||
|
to={`/profile/${friend.id}`}
|
||||||
|
className="profile-content__list-item"
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={32}
|
size={32}
|
||||||
src={friend.profileImageUrl}
|
src={friend.profileImageUrl}
|
||||||
@ -60,7 +63,7 @@ export function FriendsBox() {
|
|||||||
<div
|
<div
|
||||||
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
||||||
>
|
>
|
||||||
<span className={styles.friendName}>
|
<span className="profile-content__friend-name">
|
||||||
{friend.displayName}
|
{friend.displayName}
|
||||||
</span>
|
</span>
|
||||||
{friend.currentGame && (
|
{friend.currentGame && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.locked-profile {
|
.locked-profile {
|
||||||
&__container {
|
&__container {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { LockIcon } from "@primer/octicons-react";
|
import { LockIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./locked-profile.css";
|
import "./locked-profile.scss"
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.profile-content {
|
.profile-content {
|
||||||
&__game-cover {
|
&__game-cover {
|
||||||
@ -250,4 +250,43 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__game-card-style {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background:
|
||||||
|
linear-gradient(0deg, rgba(0,0,0 0.70) 20%, transparent 100%);
|
||||||
|
padding: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-playtime {
|
||||||
|
background-color: $background-color;
|
||||||
|
color: $muted-color;
|
||||||
|
border: solid 1px $border-color;
|
||||||
|
border-radius: 4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4;
|
||||||
|
padding: 4px,
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-card-stats-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8;
|
||||||
|
color: $muted-color;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
gap: #{$spacing-unit * 3};
|
||||||
|
padding: #{$spacing-unit * 3};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -89,16 +88,12 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
className="profile-content__container"
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
|
||||||
padding: `${SPACING_UNIT * 3}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1 }}>
|
<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,7 +103,7 @@ 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 && (
|
||||||
@ -116,7 +111,7 @@ export function ProfileContent() {
|
|||||||
)}
|
)}
|
||||||
</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 +127,7 @@ export function ProfileContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldShowRightContent && (
|
{shouldShowRightContent && (
|
||||||
<div className={styles.rightContent}>
|
<div className="profile-content__right-content">
|
||||||
<UserStatsBox />
|
<UserStatsBox />
|
||||||
<RecentGamesBox />
|
<RecentGamesBox />
|
||||||
<FriendsBox />
|
<FriendsBox />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
import "./profile-content.scss";
|
||||||
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";
|
||||||
@ -44,28 +44,28 @@ export function RecentGamesBox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="profile-content__section-header">
|
||||||
<h2>{t("activity")}</h2>
|
<h2>{t("activity")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className="profile-content__box">
|
||||||
<ul className={styles.list}>
|
<ul className="profile-content__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="profile-content__list-item"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={game.iconUrl!}
|
src={game.iconUrl!}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className={styles.listItemImage}
|
className="profile-content__list-item-image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.listItemDetails}>
|
<div className="profile-content__list-item-details">
|
||||||
<span className={styles.listItemTitle}>{game.title}</span>
|
<span className="profile-content__list-item-title">{game.title}</span>
|
||||||
|
|
||||||
<div className={styles.listItemDescription}>
|
<div className="profile-content__list-item-description">
|
||||||
<ClockIcon />
|
<ClockIcon />
|
||||||
<small>{formatPlayTime(game)}</small>
|
<small>{formatPlayTime(game)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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,12 +9,13 @@ 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 "./profile-content.scss";
|
||||||
|
|
||||||
interface UserLibraryGameCardProps {
|
interface UserLibraryGameCardProps {
|
||||||
game: UserGame;
|
game: UserGame;
|
||||||
statIndex: number;
|
statIndex: number;
|
||||||
@ -95,42 +95,18 @@ export function UserLibraryGameCard({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
}}
|
}}
|
||||||
title={game.title}
|
title={game.title}
|
||||||
className={styles.game}
|
className="profile-content__game"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
className={styles.gameCover}
|
className="profile-content__game-cover"
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
>
|
>
|
||||||
<div
|
<div className="profile-content__game-card-style">
|
||||||
style={{
|
<small className="profile-content__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>
|
||||||
@ -143,16 +119,7 @@ export function UserLibraryGameCard({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="profile-content__game-card-stats-container">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
overflow: "hidden",
|
|
||||||
height: 18,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -160,7 +127,7 @@ export function UserLibraryGameCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.gameCardStats}
|
className="profile-content__game-card-stats"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -176,7 +143,7 @@ export function UserLibraryGameCard({
|
|||||||
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
{game.achievementsPointsEarnedSum > 0 && (
|
||||||
<div
|
<div
|
||||||
className={styles.gameCardStats}
|
className="profile-content__game-card-stats"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 5,
|
gap: 5,
|
||||||
@ -203,7 +170,7 @@ export function UserLibraryGameCard({
|
|||||||
<progress
|
<progress
|
||||||
max={1}
|
max={1}
|
||||||
value={game.unlockedAchievementCount / game.achievementCount}
|
value={game.unlockedAchievementCount / game.achievementCount}
|
||||||
className={styles.achievementsProgressBar}
|
className="profile-content__achievements-progress-bar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -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,8 @@ 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 "./profile-content.scss";
|
||||||
|
|
||||||
export function UserStatsBox() {
|
export function UserStatsBox() {
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
@ -36,22 +36,22 @@ export function UserStatsBox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="profile-content__section-header">
|
||||||
<h2>{t("stats")}</h2>
|
<h2>{t("stats")}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.box}>
|
<div className="profile-content__box">
|
||||||
<ul className={styles.list}>
|
<ul className="profile-content__list">
|
||||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||||
<li className={styles.statsListItem}>
|
<li className="profile-content__list-item">
|
||||||
<h3 className={styles.listItemTitle}>
|
<h3 className="profile-content__list-item-title">
|
||||||
{t("achievements_unlocked")}
|
{t("achievements_unlocked")}
|
||||||
</h3>
|
</h3>
|
||||||
{userStats.unlockedAchievementSum !== undefined ? (
|
{userStats.unlockedAchievementSum !== undefined ? (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", justifyContent: "space-between" }}
|
style={{ display: "flex", justifyContent: "space-between" }}
|
||||||
>
|
>
|
||||||
<p className={styles.listItemDescription}>
|
<p className="profile-content__list-item-description">
|
||||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||||
{t("achievements")}
|
{t("achievements")}
|
||||||
</p>
|
</p>
|
||||||
@ -60,9 +60,9 @@ export function UserStatsBox() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
className={styles.link}
|
className="profile-content__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small style={{ color: "#ffc107" }}>
|
||||||
{t("show_achievements_on_profile")}
|
{t("show_achievements_on_profile")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
@ -71,13 +71,15 @@ export function UserStatsBox() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||||
<li className={styles.statsListItem}>
|
<li className="profile-content__stats-list-item">
|
||||||
<h3 className={styles.listItemTitle}>{t("earned_points")}</h3>
|
<h3 className="profile-content__list-item-title">
|
||||||
|
{t("earned_points")}
|
||||||
|
</h3>
|
||||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", justifyContent: "space-between" }}
|
style={{ display: "flex", justifyContent: "space-between" }}
|
||||||
>
|
>
|
||||||
<p className={styles.listItemDescription}>
|
<p className="profile-content__list-item-description">
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon width={20} height={20} />
|
||||||
{numberFormatter.format(
|
{numberFormatter.format(
|
||||||
userStats.achievementsPointsEarnedSum.value
|
userStats.achievementsPointsEarnedSum.value
|
||||||
@ -94,9 +96,9 @@ export function UserStatsBox() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements-points")}
|
onClick={() => showHydraCloudModal("achievements-points")}
|
||||||
className={styles.link}
|
className="profile-content__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small style={{ color: "#ffc107" }}>
|
||||||
{t("show_points_on_profile")}
|
{t("show_points_on_profile")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
@ -104,10 +106,12 @@ export function UserStatsBox() {
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<li className={styles.statsListItem}>
|
<li className="profile-content__list-item">
|
||||||
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
|
<h3 className="profile-content__list-item-title">
|
||||||
|
{t("total_play_time")}
|
||||||
|
</h3>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<p className={styles.listItemDescription}>
|
<p className="profile-content__list-item-description">
|
||||||
<ClockIcon />
|
<ClockIcon />
|
||||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.profile-hero {
|
.profile-hero {
|
||||||
&__content-box {
|
&__content-box {
|
||||||
@ -89,4 +89,11 @@
|
|||||||
gap: $spacing-unit;
|
gap: $spacing-unit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: #{$spacing-unit};
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import "./profile-hero.scss"
|
||||||
|
|
||||||
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 {
|
||||||
@ -127,7 +126,7 @@ export function ProfileHero() {
|
|||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => setShowEditProfileModal(true)}
|
onClick={() => setShowEditProfileModal(true)}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
style={{ borderColor: "#8e919b" }}
|
||||||
>
|
>
|
||||||
<PencilIcon />
|
<PencilIcon />
|
||||||
{t("edit_profile")}
|
{t("edit_profile")}
|
||||||
@ -152,7 +151,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 }}
|
style={{ borderColor: "#8e919b" }}
|
||||||
>
|
>
|
||||||
<PersonAddIcon />
|
<PersonAddIcon />
|
||||||
{t("add_friend")}
|
{t("add_friend")}
|
||||||
@ -187,7 +186,7 @@ export function ProfileHero() {
|
|||||||
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
style={{ borderColor: "#8e919b" }}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon />
|
<XCircleFillIcon />
|
||||||
{t("undo_friendship")}
|
{t("undo_friendship")}
|
||||||
@ -204,7 +203,7 @@ export function ProfileHero() {
|
|||||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
style={{ borderColor: "#8e919b" }}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon /> {t("cancel_request")}
|
<XCircleFillIcon /> {t("cancel_request")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -219,7 +218,7 @@ export function ProfileHero() {
|
|||||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||||
}
|
}
|
||||||
disabled={isPerformingAction}
|
disabled={isPerformingAction}
|
||||||
style={{ borderColor: vars.color.body }}
|
style={{ borderColor: "#8e919b" }}
|
||||||
>
|
>
|
||||||
<CheckCircleFillIcon /> {t("accept_request")}
|
<CheckCircleFillIcon /> {t("accept_request")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -279,7 +278,7 @@ export function ProfileHero() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className="profile-hero__content-box"
|
||||||
style={{ background: heroBackground }}
|
style={{ background: heroBackground }}
|
||||||
>
|
>
|
||||||
{backgroundImage && (
|
{backgroundImage && (
|
||||||
@ -303,10 +302,10 @@ export function ProfileHero() {
|
|||||||
zIndex: 1,
|
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 +315,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 +325,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,18 +357,13 @@ export function ProfileHero() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.heroPanel}
|
className="profile-hero__hero-panel"
|
||||||
style={{
|
style={{
|
||||||
background: backgroundImage ? backgroundImageLayer : heroBackground,
|
background: backgroundImage ? backgroundImageLayer : heroBackground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
className="profile-hero__actions"
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{profileActions}
|
{profileActions}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
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 "../../_theme.scss"
|
||||||
|
import "./profile.scss";
|
||||||
|
|
||||||
import { UserProfileContextProvider } from "@renderer/context";
|
import { UserProfileContextProvider } from "@renderer/context";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ export default function Profile() {
|
|||||||
|
|
||||||
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>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.report-profile {
|
.report-profile {
|
||||||
&__report-button {
|
&__report-button {
|
||||||
@ -10,4 +10,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
&__button {
|
||||||
|
margin-top: $spacing-unit;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__report-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
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"];
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
@ -76,11 +76,7 @@ export function ReportProfile() {
|
|||||||
clickOutsideToClose={false}
|
clickOutsideToClose={false}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
style={{
|
className="report-profile__report-modal"
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -109,7 +105,7 @@ export function ReportProfile() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={{ marginTop: `${SPACING_UNIT}px`, alignSelf: "flex-end" }}
|
className="report-profile__button"
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
{t("report")}
|
{t("report")}
|
||||||
@ -119,7 +115,7 @@ export function ReportProfile() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.reportButton}
|
className="report-profile__report-button"
|
||||||
onClick={() => setShowReportProfileModal(true)}
|
onClick={() => setShowReportProfileModal(true)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.upload-background-image-button {
|
.upload-background-image-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -3,10 +3,11 @@ 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);
|
||||||
@ -52,7 +53,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}
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
@import "../../scss/variables";
|
||||||
|
|
||||||
|
.download-source {
|
||||||
|
&__add-source {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit};
|
||||||
|
min-width: 500px,
|
||||||
|
}
|
||||||
|
|
||||||
|
&__validation-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: #{$spacing-unit * 3};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit / 2};
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@ import { useCallback, useContext, useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@ -139,12 +138,7 @@ export function AddDownloadSourceModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
className="download-source__add-source"
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
minWidth: "500px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register("url")}
|
{...register("url")}
|
||||||
@ -166,19 +160,10 @@ export function AddDownloadSourceModal({
|
|||||||
|
|
||||||
{validationResult && (
|
{validationResult && (
|
||||||
<div
|
<div
|
||||||
style={{
|
className="download-source__validation-result"
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
marginTop: `${SPACING_UNIT * 3}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
className="download-source__input"
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<h4>{validationResult?.name}</h4>
|
<h4>{validationResult?.name}</h4>
|
||||||
<small>
|
<small>
|
||||||
|
@ -5,7 +5,7 @@ import { AddThemeModal } from "./add-theme-modal";
|
|||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
import { PlusCircleIcon, GlobeIcon, PencilIcon } from "@primer/octicons-react";
|
import { PlusCircleIcon, GlobeIcon, PencilIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./settings-download-sources.css";
|
import "./settings-download-sources";
|
||||||
|
|
||||||
export function SettingsAppearance() {
|
export function SettingsAppearance() {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@ -32,7 +32,7 @@ export function SettingsAppearance() {
|
|||||||
|
|
||||||
<p>{t("themes_description")}</p>
|
<p>{t("themes_description")}</p>
|
||||||
|
|
||||||
<div className={styles.downloadSourcesHeader}>
|
<div className="settings-download-sources__download-source-item-header">
|
||||||
<div style={{ display: "flex", gap: "8px" }}>
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
<Button type="button" theme="outline">
|
<Button type="button" theme="outline">
|
||||||
<GlobeIcon />
|
<GlobeIcon />
|
||||||
|
@ -37,4 +37,24 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: $spacing-unit;
|
gap: $spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
gap: #{$spacing-unit};
|
||||||
|
margin-top: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subscription-description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subscription {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Avatar, Button, SelectField } from "@renderer/components";
|
import { Avatar, Button, SelectField } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
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 styles from "./settings-account.css";
|
|
||||||
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
|
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@ -15,6 +13,8 @@ import {
|
|||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
|
import "./settings-account.scss";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
|
||||||
}
|
}
|
||||||
@ -145,7 +145,7 @@ export function SettingsAccount() {
|
|||||||
if (!userDetails) return null;
|
if (!userDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
<form className="settings-account__form" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="profileVisibility"
|
name="profileVisibility"
|
||||||
@ -181,15 +181,7 @@ export function SettingsAccount() {
|
|||||||
<h4>{t("current_email")}</h4>
|
<h4>{t("current_email")}</h4>
|
||||||
<p>{userDetails?.email ?? t("no_email_account")}</p>
|
<p>{userDetails?.email ?? t("no_email_account")}</p>
|
||||||
|
|
||||||
<div
|
<div className="settings-account__actions">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "start",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
marginTop: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
|
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
|
||||||
@ -210,21 +202,9 @@ export function SettingsAccount() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section className="settings-account__subscription">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3>Hydra Cloud</h3>
|
<h3>Hydra Cloud</h3>
|
||||||
<div
|
<div className="settings-account__subscription-description">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getHydraCloudSectionContent().description}
|
{getHydraCloudSectionContent().description}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -240,27 +220,15 @@ export function SettingsAccount() {
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section className="settings-account__subscription-description">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3>{t("blocked_users")}</h3>
|
<h3>{t("blocked_users")}</h3>
|
||||||
|
|
||||||
{blockedUsers.length > 0 ? (
|
{blockedUsers.length > 0 ? (
|
||||||
<ul className={styles.blockedUsersList}>
|
<ul className="settings-account__blocked-users-list">
|
||||||
{blockedUsers.map((user) => {
|
{blockedUsers.map((user) => {
|
||||||
return (
|
return (
|
||||||
<li key={user.id} className={styles.blockedUser}>
|
<li key={user.id} className="settings-account__blocked-user">
|
||||||
<div
|
<div className="settings-account__subscription">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
style={{ filter: "grayscale(100%)" }}
|
style={{ filter: "grayscale(100%)" }}
|
||||||
size={32}
|
size={32}
|
||||||
@ -272,7 +240,7 @@ export function SettingsAccount() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.unblockButton}
|
className="settings-account__unblock-button"
|
||||||
onClick={() => handleUnblockClick(user.id)}
|
onClick={() => handleUnblockClick(user.id)}
|
||||||
disabled={isUnblocking}
|
disabled={isUnblocking}
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,6 @@ import { useContext, useEffect, useState } from "react";
|
|||||||
import { TextField, Button, Badge } from "@renderer/components";
|
import { TextField, Button, Badge } from "@renderer/components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./settings-download-sources.css";
|
|
||||||
import type { DownloadSource } from "@types";
|
import type { DownloadSource } from "@types";
|
||||||
import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
|
import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
|
||||||
import { AddDownloadSourceModal } from "./add-download-source-modal";
|
import { AddDownloadSourceModal } from "./add-download-source-modal";
|
||||||
@ -14,6 +13,7 @@ import { downloadSourcesTable } from "@renderer/dexie";
|
|||||||
import { downloadSourcesWorker } from "@renderer/workers";
|
import { downloadSourcesWorker } from "@renderer/workers";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { setFilters, clearFilters } from "@renderer/features";
|
import { setFilters, clearFilters } from "@renderer/features";
|
||||||
|
import "./settings-download-sources.scss";
|
||||||
|
|
||||||
export function SettingsDownloadSources() {
|
export function SettingsDownloadSources() {
|
||||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||||
@ -118,7 +118,7 @@ export function SettingsDownloadSources() {
|
|||||||
|
|
||||||
<p>{t("download_sources_description")}</p>
|
<p>{t("download_sources_description")}</p>
|
||||||
|
|
||||||
<div className={styles.downloadSourcesHeader}>
|
<div className="settings-download-sources__download-sources-header">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
@ -144,15 +144,13 @@ export function SettingsDownloadSources() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className={styles.downloadSources}>
|
<ul className="settings-download-sources__download-sources">
|
||||||
{downloadSources.map((downloadSource) => (
|
{downloadSources.map((downloadSource) => (
|
||||||
<li
|
<li
|
||||||
key={downloadSource.id}
|
key={downloadSource.id}
|
||||||
className={styles.downloadSourceItem({
|
className="settings-download-sources__download-source-item"
|
||||||
isSyncing: isSyncingDownloadSources,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<div className={styles.downloadSourceItemHeader}>
|
<div className="settings-download-sources__download-source-item-header">
|
||||||
<h2>{downloadSource.name}</h2>
|
<h2>{downloadSource.name}</h2>
|
||||||
|
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
@ -161,7 +159,7 @@ export function SettingsDownloadSources() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.navigateToCatalogueButton}
|
className="settings-download-sources__navigate-to-catalogue-button"
|
||||||
disabled={!downloadSource.fingerprint}
|
disabled={!downloadSource.fingerprint}
|
||||||
onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
|
onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
|
||||||
>
|
>
|
||||||
|
@ -10,4 +10,13 @@
|
|||||||
&__description {
|
&__description {
|
||||||
margin-bottom: #{$spacing-unit * 2};
|
margin-bottom: #{$spacing-unit * 2};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__field-spacing {
|
||||||
|
margin-top: #{$spacing-unit};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit-button {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ import { useContext, useEffect, useState } from "react";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
||||||
import * as styles from "./settings-real-debrid.css";
|
|
||||||
|
|
||||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
|
import "./settings-real-debrid.scss"
|
||||||
|
|
||||||
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
|
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
|
||||||
|
|
||||||
@ -78,8 +77,8 @@ export function SettingsRealDebrid() {
|
|||||||
(form.useRealDebrid && !form.realDebridApiToken) || isLoading;
|
(form.useRealDebrid && !form.realDebridApiToken) || isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={styles.form} onSubmit={handleFormSubmit}>
|
<form className="settings-real-debrid__form" onSubmit={handleFormSubmit}>
|
||||||
<p className={styles.description}>{t("real_debrid_description")}</p>
|
<p className="settings-real-debrid__description">{t("real_debrid_description")}</p>
|
||||||
|
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
label={t("enable_real_debrid")}
|
label={t("enable_real_debrid")}
|
||||||
@ -101,7 +100,7 @@ export function SettingsRealDebrid() {
|
|||||||
setForm({ ...form, realDebridApiToken: event.target.value })
|
setForm({ ...form, realDebridApiToken: event.target.value })
|
||||||
}
|
}
|
||||||
placeholder="API Token"
|
placeholder="API Token"
|
||||||
containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }}
|
containerProps={{ className: "settings-real-debrid__field-spacing" }}
|
||||||
hint={
|
hint={
|
||||||
<Trans i18nKey="real_debrid_api_token_hint" ns="settings">
|
<Trans i18nKey="real_debrid_api_token_hint" ns="settings">
|
||||||
<Link to={REAL_DEBRID_API_TOKEN_URL} />
|
<Link to={REAL_DEBRID_API_TOKEN_URL} />
|
||||||
@ -112,7 +111,7 @@ export function SettingsRealDebrid() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
|
className="settings-real-debrid__submit-button"
|
||||||
disabled={isButtonDisabled}
|
disabled={isButtonDisabled}
|
||||||
>
|
>
|
||||||
{t("save_changes")}
|
{t("save_changes")}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./settings.css";
|
import "./settings.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||||
import { SettingsGeneral } from "./settings-general";
|
import { SettingsGeneral } from "./settings-general";
|
||||||
@ -63,9 +63,9 @@ export default function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className="settings__container">
|
||||||
<div className={styles.content}>
|
<div className="settings__content">
|
||||||
<section className={styles.settingsCategories}>
|
<section className="settings__categories">
|
||||||
{categories.map((category, index) => (
|
{categories.map((category, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={category}
|
key={category}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./hydra-cloud.scss";
|
||||||
|
|
||||||
export interface HydraCloudModalProps {
|
export interface HydraCloudModalProps {
|
||||||
feature: string;
|
feature: string;
|
||||||
@ -23,12 +23,7 @@ export const HydraCloudModal = ({
|
|||||||
<Modal visible={visible} title={t("hydra_cloud")} onClose={onClose}>
|
<Modal visible={visible} title={t("hydra_cloud")} onClose={onClose}>
|
||||||
<div
|
<div
|
||||||
data-hydra-cloud-feature={feature}
|
data-hydra-cloud-feature={feature}
|
||||||
style={{
|
className="hydra-cloud__on-close"
|
||||||
display: "flex",
|
|
||||||
width: "500px",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("hydra_cloud_feature_found")}
|
{t("hydra_cloud_feature_found")}
|
||||||
<Button onClick={handleClickOpenCheckout}>{t("learn_more")}</Button>
|
<Button onClick={handleClickOpenCheckout}>{t("learn_more")}</Button>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
|
.hydra-cloud {
|
||||||
|
&__on-close {
|
||||||
|
display: flex;
|
||||||
|
width: 500px;
|
||||||
|
flex-direction: "column";
|
||||||
|
gap: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
}
|
@ -58,14 +58,14 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={styles.acceptRequestButton}
|
className="user-friend-modal__accept-request-button"
|
||||||
onClick={() => props.onClickAcceptRequest(userId)}
|
onClick={() => props.onClickAcceptRequest(userId)}
|
||||||
title={t("accept_request")}
|
title={t("accept_request")}
|
||||||
>
|
>
|
||||||
<CheckCircleIcon size={28} />
|
<CheckCircleIcon size={28} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.cancelRequestButton}
|
className="user-friend-modal__cancel-request-button"
|
||||||
onClick={() => props.onClickRefuseRequest(userId)}
|
onClick={() => props.onClickRefuseRequest(userId)}
|
||||||
title={t("ignore_request")}
|
title={t("ignore_request")}
|
||||||
>
|
>
|
||||||
@ -78,7 +78,7 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
if (type === "ACCEPTED") {
|
if (type === "ACCEPTED") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={styles.cancelRequestButton}
|
className="user-friend-modal__cancel-request-button"
|
||||||
onClick={() => props.onClickUndoFriendship(userId)}
|
onClick={() => props.onClickUndoFriendship(userId)}
|
||||||
title={t("undo_friendship")}
|
title={t("undo_friendship")}
|
||||||
>
|
>
|
||||||
@ -90,7 +90,7 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
if (type === "BLOCKED") {
|
if (type === "BLOCKED") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={styles.cancelRequestButton}
|
className="user-friend-modal__cancel-request-button"
|
||||||
onClick={() => props.onClickUnblock(userId)}
|
onClick={() => props.onClickUnblock(userId)}
|
||||||
title={t("unblock")}
|
title={t("unblock")}
|
||||||
>
|
>
|
||||||
@ -104,8 +104,8 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
|
|
||||||
if (type === "BLOCKED") {
|
if (type === "BLOCKED") {
|
||||||
return (
|
return (
|
||||||
<div className={styles.friendListContainer}>
|
<div className="user-friend-modal__friend-list-container">
|
||||||
<div className={styles.friendListButton} style={{ cursor: "inherit" }}>
|
<div className="user-friend-modal__friend-list-button" style={{ cursor: "inherit" }}>
|
||||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -117,18 +117,13 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className={styles.friendListDisplayName}>{displayName}</p>
|
<p className="user-friend-modal__friend-list-display-name">
|
||||||
|
{displayName}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="user-friend-modal__friend-list-actions">
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: "8px",
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getRequestActions()}
|
{getRequestActions()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,10 +131,10 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.friendListContainer}>
|
<div className="user-friend-modal__friend-list-container">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendListButton}
|
className="user-friend-modal__friend-list-button"
|
||||||
onClick={() => props.onClickItem(userId)}
|
onClick={() => props.onClickItem(userId)}
|
||||||
>
|
>
|
||||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||||
@ -152,19 +147,14 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className={styles.friendListDisplayName}>{displayName}</p>
|
<p className="user-friend-modal__friend-list-display-name">
|
||||||
|
{displayName}
|
||||||
|
</p>
|
||||||
{getRequestDescription()}
|
{getRequestDescription()}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div className="user-friend-modal__friend-list-actions">
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: "8px",
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getRequestActions()}
|
{getRequestActions()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Button, TextField } from "@renderer/components";
|
import { Button, TextField } from "@renderer/components";
|
||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { UserFriendItem } from "./user-friend-item";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
|
import "./user-friend-modal.scss";
|
||||||
|
|
||||||
export interface UserFriendModalAddFriendProps {
|
export interface UserFriendModalAddFriendProps {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
@ -76,26 +76,20 @@ export const UserFriendModalAddFriend = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="user-friend-modal__add-friend-controls">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t("friend_code")}
|
label={t("friend_code")}
|
||||||
value={friendCode}
|
value={friendCode}
|
||||||
minLength={8}
|
minLength={8}
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
containerProps={{ style: { width: "100%" } }}
|
containerProps={{
|
||||||
|
className: "user-friend-modal__text-field-container",
|
||||||
|
}}
|
||||||
onChange={(e) => setFriendCode(e.target.value)}
|
onChange={(e) => setFriendCode(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={isAddingFriend}
|
disabled={isAddingFriend}
|
||||||
style={{ alignSelf: "end" }}
|
className="user-friend-modal__button-align"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClickAddFriend}
|
onClick={handleClickAddFriend}
|
||||||
>
|
>
|
||||||
@ -105,20 +99,14 @@ export const UserFriendModalAddFriend = ({
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleClickSeeProfile}
|
onClick={handleClickSeeProfile}
|
||||||
disabled={isAddingFriend}
|
disabled={isAddingFriend}
|
||||||
style={{ alignSelf: "end" }}
|
className="user-friend-modal__button-align"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{t("see_profile")}
|
{t("see_profile")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="user-friend-modal__pending-container">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3>{t("pending")}</h3>
|
<h3>{t("pending")}</h3>
|
||||||
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
||||||
{friendRequests.map((request) => {
|
{friendRequests.map((request) => {
|
||||||
@ -139,4 +127,4 @@ export const UserFriendModalAddFriend = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,4 +1,3 @@
|
|||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import type { UserFriend } from "@types";
|
import type { UserFriend } from "@types";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { UserFriendItem } from "./user-friend-item";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
@ -6,6 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
import "./user-friend-modal.scss";
|
||||||
|
|
||||||
export interface UserFriendModalListProps {
|
export interface UserFriendModalListProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -94,18 +94,17 @@ export const UserFriendModalList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor="var(--color-background)" highlightColor="#444">
|
||||||
<div
|
<div
|
||||||
ref={listContainer}
|
ref={listContainer}
|
||||||
|
className="user-friend-modal__friend-list-container"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
maxHeight: "400px",
|
maxHeight: "400px",
|
||||||
overflowY: "scroll",
|
overflowY: "scroll",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isLoading && friends.length === 0 && <p>{t("no_friends_added")}</p>}
|
{!isLoading && friends.length === 0 &&
|
||||||
|
<p className="user-friend-modal__friend-list-display-name">{t("no_friends_added")}</p>}
|
||||||
{friends.map((friend) => {
|
{friends.map((friend) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendItem
|
<UserFriendItem
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../../scss/variables";
|
@import "../../../scss/variables";
|
||||||
|
|
||||||
.user-friend-modal {
|
.user-friend-modal {
|
||||||
&__friend-list-display-name {
|
&__friend-list-display-name {
|
||||||
@ -40,6 +40,13 @@
|
|||||||
padding: 0 $spacing-unit;
|
padding: 0 $spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__friend-list-actions {
|
||||||
|
position: "absolute";
|
||||||
|
right: "8px";
|
||||||
|
display: "flex";
|
||||||
|
gap: #{$spacing-unit}
|
||||||
|
}
|
||||||
|
|
||||||
&__friend-request-item {
|
&__friend-request-item {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
|
|
||||||
@ -82,4 +89,25 @@
|
|||||||
color: $muted-color;
|
color: $muted-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&__add-friend-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: #{$spacing-unit};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text-field-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-align {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pending-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{$spacing-unit * 2};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,3 +36,8 @@ $body-font-size: 14px;
|
|||||||
$small-font-size: 12px;
|
$small-font-size: 12px;
|
||||||
|
|
||||||
$app-container: app-container;
|
$app-container: app-container;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background-color: #{$background-color};
|
||||||
|
--spacing-unit: #{$spacing-unit};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user