migration to scss

This commit is contained in:
Nate 2025-01-17 19:26:27 -03:00
parent 691dba26af
commit b0eb7c16cd
78 changed files with 739 additions and 739 deletions

View File

@ -1,4 +1,4 @@
@import "../../scss/variables"; @import "./scss/variables";
* { * {
box-sizing: border-box; box-sizing: border-box;

View File

@ -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;
} }

View File

@ -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}

View File

@ -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;
} }

View File

@ -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

View File

@ -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);
} }

View File

@ -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,

View File

@ -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;
} }
} }

View File

@ -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}

View File

@ -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);
} }

View File

@ -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">

View File

@ -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: "???",

View File

@ -49,7 +49,7 @@
background-color: $color-muted; background-color: $color-muted;
} }
&--disabled { &__disabled {
opacity: $opacity-disabled; opacity: $opacity-disabled;
} }
} }

View File

@ -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>

View File

@ -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}>

View File

@ -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>
); );
} }

View File

@ -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 ? (

View File

@ -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",
}} }}
> >

View File

@ -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>

View File

@ -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({

View File

@ -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>

View File

@ -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;
} }

View File

@ -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>
); );

View File

@ -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)}

View File

@ -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={{

View File

@ -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;
}
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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} />
))} ))}

View File

@ -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);
} }
} }

View File

@ -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}

View File

@ -1,4 +1,4 @@
@import "../../scss/variables"; @import "../../../scss/variables";
.hero-panel-actions { .hero-panel-actions {
&__action { &__action {

View File

@ -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")}

View File

@ -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")}

View File

@ -1,4 +1,4 @@
@import "../../scss/variables"; @import "../../../scss/variables";
.hero-panel { .hero-panel {
width: 100%; width: 100%;

View File

@ -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>
</> </>
); );
} }

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
} }

View File

@ -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

View File

@ -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;
}
} }

View File

@ -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")}

View File

@ -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 && (

View File

@ -1,4 +1,4 @@
@import "../../scss/variables"; @import "../../../scss/variables";
.locked-profile { .locked-profile {
&__container { &__container {

View File

@ -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>

View File

@ -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};
}
} }

View File

@ -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 />

View File

@ -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>

View File

@ -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>
)} )}

View File

@ -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>

View File

@ -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;
}
} }

View File

@ -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>

View File

@ -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>

View File

@ -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};
}
} }

View File

@ -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}
> >

View File

@ -1,4 +1,4 @@
@import "../../scss/variables"; @import "../../../scss/variables";
.upload-background-image-button { .upload-background-image-button {
position: absolute; position: absolute;

View File

@ -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}
> >

View File

@ -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};
}
}

View File

@ -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>

View File

@ -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 />

View File

@ -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};
}
} }

View File

@ -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}
> >

View File

@ -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)}
> >

View File

@ -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};
}
} }

View File

@ -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")}

View File

@ -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}

View File

@ -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>

View File

@ -0,0 +1,10 @@
@import "../../../scss/variables";
.hydra-cloud {
&__on-close {
display: flex;
width: 500px;
flex-direction: "column";
gap: #{$spacing-unit * 2};
}
}

View File

@ -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>

View File

@ -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>
</> </>
); );
}; };

View File

@ -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

View File

@ -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};
}
} }

View File

@ -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};
}