full migration to scss

This commit is contained in:
Nate 2025-01-17 19:43:41 -03:00
parent b0eb7c16cd
commit d038398750
45 changed files with 310 additions and 396 deletions

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

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

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

@ -5,7 +5,7 @@ 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 "./achievements.scss"; import "./achievements.scss";
import "../../scss/_variables.scss" import "../../scss/_variables.scss";
interface AchievementListProps { interface AchievementListProps {
achievements: UserAchievement[]; achievements: UserAchievement[];

View File

@ -4,7 +4,6 @@ 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 "./achievement-panel.sccs"; import "./achievement-panel.sccs";
export interface AchievementPanelProps { export interface AchievementPanelProps {

View File

@ -8,18 +8,18 @@ import {
formatDownloadProgress, formatDownloadProgress,
} from "@renderer/helpers"; } from "@renderer/helpers";
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import type { ComparedAchievements } from "@types"; import type { ComparedAchievements } from "@types";
import { average } from "color.js"; import { average } from "color.js";
import Color from "color"; import Color from "color";
import { Link } from "@renderer/components"; import { Link } from "@renderer/components";
import { ComparedAchievementList } from "./compared-achievement-list"; import { ComparedAchievementList } from "./compared-achievement-list";
import * as styles from "./achievements.css";
import { AchievementList } from "./achievement-list"; import { AchievementList } from "./achievement-list";
import { AchievementPanel } from "./achievement-panel"; import { AchievementPanel } from "./achievement-panel";
import { ComparedAchievementPanel } from "./compared-achievement-panel"; import { ComparedAchievementPanel } from "./compared-achievement-panel";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
import "./achievements.scss";
import "../../scss/_variables.scss";
interface UserInfo { interface UserInfo {
id: string; id: string;
@ -48,10 +48,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
user: Pick<UserInfo, "profileImageUrl" | "displayName"> user: Pick<UserInfo, "profileImageUrl" | "displayName">
) => { ) => {
return ( return (
<div className={styles.profileAvatar}> <div className="achievements__profile-avatar">
{user.profileImageUrl ? ( {user.profileImageUrl ? (
<img <img
className={styles.profileAvatar} className="achievements__profile-avatar"
src={user.profileImageUrl} src={user.profileImageUrl}
alt={user.displayName} alt={user.displayName}
/> />
@ -64,97 +64,38 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) { if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
return ( return (
<div <div className="achievements__summary achievements__summary--locked">
style={{ <div className="achievements__summary-overlay">
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
position: "relative",
padding: `${SPACING_UNIT}px`,
}}
>
<div
style={{
position: "absolute",
zIndex: 2,
inset: 0,
width: "100%",
height: "100%",
background: "rgba(0, 0, 0, 0.7)",
display: "flex",
alignItems: "center",
flexDirection: "row",
gap: `${SPACING_UNIT}px`,
borderRadius: "4px",
justifyContent: "center",
}}
>
<LockIcon size={24} /> <LockIcon size={24} />
<h3> <h3>
<button <button
className={styles.subscriptionRequiredButton} className="achievements__subscription-required-button"
onClick={() => showHydraCloudModal("achievements")} onClick={() => showHydraCloudModal("achievements")}
> >
{t("subscription_needed")} {t("subscription_needed")}
</button> </button>
</h3> </h3>
</div> </div>
<div <div className="achievements__summary-content achievements__summary-content--blurred">
style={{
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
height: "62px",
position: "relative",
filter: "blur(4px)",
}}
>
{getProfileImage(user)} {getProfileImage(user)}
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1> <h1 className="achievements__summary-title">{user.displayName}</h1>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div <div className="achievements__summary">
style={{
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
padding: `${SPACING_UNIT}px`,
}}
>
{getProfileImage(user)} {getProfileImage(user)}
<div <div className="achievements__summary-details">
style={{ <h1 className="achievements__summary-title">{user.displayName}</h1>
display: "flex", <div className="achievements__summary-stats">
flexDirection: "column", <div className="achievements__summary-count">
width: "100%",
}}
>
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: 8,
color: "#c0c1c7",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<TrophyIcon size={13} /> <TrophyIcon size={13} />
<span> <span>
{user.unlockedAchievementCount} / {user.totalAchievementCount} {user.unlockedAchievementCount} / {user.totalAchievementCount}
</span> </span>
</div> </div>
<span> <span>
{formatDownloadProgress( {formatDownloadProgress(
user.unlockedAchievementCount / user.totalAchievementCount user.unlockedAchievementCount / user.totalAchievementCount
@ -164,7 +105,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
<progress <progress
max={1} max={1}
value={user.unlockedAchievementCount / user.totalAchievementCount} value={user.unlockedAchievementCount / user.totalAchievementCount}
className={styles.achievementsProgressBar} className="achievements__progress-bar"
/> />
</div> </div>
</div> </div>
@ -201,28 +142,29 @@ export function AchievementsContent({
setGameColor(backgroundColor); setGameColor(backgroundColor);
}; };
const HERO_HEIGHT = 150;
const onScroll: React.UIEventHandler<HTMLElement> = (event) => { const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
const scrollY = (event.target as HTMLDivElement).scrollTop; const scrollY = (event.target as HTMLDivElement).scrollTop;
if (scrollY >= heroHeight && !isHeaderStuck) { if (scrollY >= heroHeight && !isHeaderStuck) {
setIsHeaderStuck(true); setIsHeaderStuck(true);
} }
if (scrollY <= heroHeight && isHeaderStuck) { if (scrollY <= heroHeight && isHeaderStuck) {
setIsHeaderStuck(false); setIsHeaderStuck(false);
} }
}; };
const getProfileImage = ( const getProfileImage = (
user: Pick<UserInfo, "profileImageUrl" | "displayName"> user: Pick<UserInfo, "profileImageUrl" | "displayName">
) => { ) => {
return ( return (
<div className={styles.profileAvatarSmall}> <div className="achievements__profile-avatar-small">
{user.profileImageUrl ? ( {user.profileImageUrl ? (
<img <img
className={styles.profileAvatarSmall} className="achievements__profile-avatar-small"
src={user.profileImageUrl} src={user.profileImageUrl}
alt={user.displayName} alt={user.displayName}
/> />
@ -236,10 +178,10 @@ export function AchievementsContent({
if (!objectId || !shop || !gameTitle || !userDetails) return null; if (!objectId || !shop || !gameTitle || !userDetails) return null;
return ( return (
<div className={styles.wrapper}> <div className="achievements__wrapper">
<img <img
src={steamUrlBuilder.libraryHero(objectId)} src={steamUrlBuilder.libraryHero(objectId)}
style={{ display: "none" }} className="achievements__hidden-image"
alt={gameTitle} alt={gameTitle}
onLoad={handleHeroLoad} onLoad={handleHeroLoad}
/> />
@ -247,38 +189,29 @@ export function AchievementsContent({
<section <section
ref={containerRef} ref={containerRef}
onScroll={onScroll} onScroll={onScroll}
className={styles.container} className="achievements__container"
> >
<div <div
className="achievements__gradient-background"
style={{ style={{
display: "flex",
flexDirection: "column",
background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`, background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`,
}} }}
> >
<div ref={heroRef} className={styles.hero}> <div ref={heroRef} className="achievements__hero">
<div className={styles.heroContent}> <div className="achievements__hero-content">
<Link <Link
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })} to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
> >
<img <img
src={steamUrlBuilder.logo(objectId)} src={steamUrlBuilder.logo(objectId)}
className={styles.gameLogo} className="achievements__game-logo"
alt={gameTitle} alt={gameTitle}
/> />
</Link> </Link>
</div> </div>
</div> </div>
<div <div className="achievements__summary-container">
style={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT}px`,
}}
>
<AchievementSummary <AchievementSummary
user={{ user={{
...userDetails, ...userDetails,
@ -298,24 +231,24 @@ export function AchievementsContent({
</div> </div>
{otherUser && ( {otherUser && (
<div className={styles.tableHeader({ stuck: isHeaderStuck })}> <div
className={classNames("achievements__table-header", {
"achievements__table-header--stuck": isHeaderStuck,
})}
>
<div <div
style={{ className={classNames("achievements__grid-container", {
display: "grid", "achievements__grid-container--no-subscription":
gridTemplateColumns: hasActiveSubscription !hasActiveSubscription,
? "3fr 1fr 1fr" })}
: "3fr 2fr",
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
}}
> >
<div></div> <div></div>
{hasActiveSubscription && ( {hasActiveSubscription && (
<div style={{ display: "flex", justifyContent: "center" }}> <div className="achievements__profile-center">
{getProfileImage({ ...userDetails })} {getProfileImage({ ...userDetails })}
</div> </div>
)} )}
<div style={{ display: "flex", justifyContent: "center" }}> <div className="achievements__profile-center">
{getProfileImage(otherUser)} {getProfileImage(otherUser)}
</div> </div>
</div> </div>
@ -336,4 +269,4 @@ export function AchievementsContent({
</section> </section>
</div> </div>
); );
} }

View File

@ -76,10 +76,7 @@ export default function Achievements() {
(otherUserId && comparedAchievements === null); (otherUserId && comparedAchievements === null);
return ( return (
<SkeletonTheme <SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
baseColor="#1c1c1c"
highlightColor="#444"
>
{showSkeleton ? ( {showSkeleton ? (
<AchievementsSkeleton /> <AchievementsSkeleton />
) : ( ) : (

View File

@ -19,4 +19,10 @@
border: 1px solid $border-color; border: 1px solid $border-color;
align-self: flex-start; align-self: flex-start;
} }
&__header {
display: flex;
gap: calc(var(--spacing-unit) * 2);
justify-content: space-between;
}
} }

View File

@ -9,8 +9,8 @@ import {
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import "./catalogue.scss"; import "./catalogue.scss";
import "../../scss/_variables.scss";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesTable } from "@renderer/dexie";
import { FilterSection } from "./filter-section"; import { FilterSection } from "./filter-section";
import { setFilters, setPage } from "@renderer/features"; import { setFilters, setPage } from "@renderer/features";
@ -270,13 +270,7 @@ export default function Catalogue() {
</div> </div>
</div> </div>
<div <div className="catalogue__header">
style={{
display: "flex",
gap: SPACING_UNIT * 2,
justifyContent: "space-between",
}}
>
<div <div
style={{ style={{
display: "flex", display: "flex",
@ -287,8 +281,8 @@ export default function Catalogue() {
> >
{isLoading ? ( {isLoading ? (
<SkeletonTheme <SkeletonTheme
baseColor={vars.color.darkBackground} baseColor="var(--dark-background-color)"
highlightColor={vars.color.background} highlightColor="var(--background-color)"
> >
{Array.from({ length: PAGE_SIZE }).map((_, i) => ( {Array.from({ length: PAGE_SIZE }).map((_, i) => (
<Skeleton <Skeleton
@ -296,7 +290,7 @@ export default function Catalogue() {
style={{ style={{
height: 105, height: 105,
borderRadius: 4, borderRadius: 4,
border: `solid 1px ${vars.color.border}`, border: `solid 1px var(--border-color)`,
}} }}
/> />
))} ))}

View File

@ -1,5 +1,5 @@
import { vars } from "@renderer/theme.css";
import { XIcon } from "@primer/octicons-react"; import { XIcon } from "@primer/octicons-react";
import "../../scss/_variables.scss";
interface FilterItemProps { interface FilterItemProps {
filter: string; filter: string;
@ -13,11 +13,11 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
color: vars.color.body, color: "var(--body-color)",
backgroundColor: vars.color.darkBackground, backgroundColor: "var(--dark-background-color",
padding: "6px 12px", padding: "6px 12px",
borderRadius: 4, borderRadius: 4,
border: `solid 1px ${vars.color.border}`, border: `solid 1px var(--border-color)`,
fontSize: 12, fontSize: 12,
}} }}
> >
@ -35,7 +35,7 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
type="button" type="button"
onClick={onRemove} onClick={onRemove}
style={{ style={{
color: vars.color.body, color: "var(--body-color)",
marginLeft: 4, marginLeft: 4,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",

View File

@ -3,8 +3,8 @@ import { useFormat } from "@renderer/hooks";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import List from "rc-virtual-list"; import List from "rc-virtual-list";
import { vars } from "@renderer/theme.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "../../scss/_variables.scss";
export interface FilterSectionProps { export interface FilterSectionProps {
title: string; title: string;
@ -80,7 +80,7 @@ export function FilterSection({
fontSize: 12, fontSize: 12,
marginBottom: 12, marginBottom: 12,
display: "block", display: "block",
color: vars.color.body, color: "var(--body-color)",
cursor: "pointer", cursor: "pointer",
textDecoration: "underline", textDecoration: "underline",
}} }}

View File

@ -30,7 +30,6 @@ export function DeleteGameModal({
onClose={onClose} onClose={onClose}
> >
<div className="delete-game-modal__actions-buttons-ctn"> <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,6 @@ 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 "./download-group.scss"; import "./download-group.scss";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@ -18,8 +18,8 @@ import { useTranslation } from "react-i18next";
import { AxiosProgressEvent } from "axios"; import { AxiosProgressEvent } from "axios";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import "./cloud-sync-modal.scss" import "./cloud-sync-modal.scss";
import "../../../scss/_variables.scss" import "../../../scss/_variables.scss";
export interface CloudSyncModalProps export interface CloudSyncModalProps
extends Omit<ModalProps, "children" | "title"> {} extends Omit<ModalProps, "children" | "title"> {}

View File

@ -2,7 +2,7 @@ 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 "./gallery-slider.scss" import "./gallery-slider.scss";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
const getButtonClasses = (visible: boolean, direction: "left" | "right") => { const getButtonClasses = (visible: boolean, direction: "left" | "right") => {

View File

@ -31,7 +31,6 @@ export function GameDetailsContent() {
game, game,
gameColor, gameColor,
setGameColor, setGameColor,
hasNSFWContentBlocked,
} = useContext(gameDetailsContext); } = useContext(gameDetailsContext);
const { showHydraCloudModal } = useSubscription(); const { showHydraCloudModal } = useSubscription();
@ -78,26 +77,27 @@ export function GameDetailsContent() {
useEffect(() => { useEffect(() => {
setBackdropOpacity(1); setBackdropOpacity(1);
}, [objectId]); }, [objectId]);
const HERO_HEIGHT = 150;
const onScroll: React.UIEventHandler<HTMLElement> = (event) => { const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
const scrollY = (event.target as HTMLDivElement).scrollTop; const scrollY = (event.target as HTMLDivElement).scrollTop;
const opacity = Math.max( const opacity = Math.max(
0, 0,
1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD) 1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD)
); );
if (scrollY >= heroHeight && !isHeaderStuck) { if (scrollY >= heroHeight && !isHeaderStuck) {
setIsHeaderStuck(true); setIsHeaderStuck(true);
} }
if (scrollY <= heroHeight && isHeaderStuck) { if (scrollY <= heroHeight && isHeaderStuck) {
setIsHeaderStuck(false); setIsHeaderStuck(false);
} }
setBackdropOpacity(opacity); setBackdropOpacity(opacity);
}; };
const handleCloudSaveButtonClick = () => { const handleCloudSaveButtonClick = () => {
if (!userDetails) { if (!userDetails) {

View File

@ -24,7 +24,6 @@ $hero-height: 300px;
&__blurred-content { &__blurred-content {
filter: blur(20px); filter: blur(20px);
} }
} }

View File

@ -12,10 +12,8 @@ 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 "./game-details.scss"; import "./game-details.scss";
import { GameDetailsContent } from "./game-details-content"; import { GameDetailsContent } from "./game-details-content";
import { import {
CloudSyncContextConsumer, CloudSyncContextConsumer,
@ -149,10 +147,7 @@ export default function GameDetails() {
)} )}
</CloudSyncContextConsumer> </CloudSyncContextConsumer>
<SkeletonTheme <SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
baseColor="#1c1c1c"
highlightColor="#444"
>
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />} {isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
<RepacksModal <RepacksModal

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 "./hero-panel-actions.scss" import "./hero-panel-actions.scss";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";

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 "./hero-panel.scss" 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";

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 "./hero-panel.scss" 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";
@ -88,4 +88,4 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
</div> </div>
</> </>
); );
} }

View File

@ -9,7 +9,7 @@ import type { GameRepack } from "@types";
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 "./download-settings-modal.scss";
import "../../../scss/_variables.scss" import "../../../scss/_variables.scss";
export interface DownloadSettingsModalProps { export interface DownloadSettingsModalProps {
visible: boolean; visible: boolean;

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";
interface RemoveGameFromLibraryModalProps { interface RemoveGameFromLibraryModalProps {
visible: boolean; visible: boolean;

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 type { Game } from "@types"; import type { Game } from "@types";
import "./remove-from-library-modal.scss" import "./remove-from-library-modal.scss";
type ResetAchievementsModalProps = Readonly<{ type ResetAchievementsModalProps = Readonly<{
visible: boolean; visible: boolean;
game: Game; game: Game;

View File

@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
import type { HowLongToBeatCategory } from "@types"; import type { HowLongToBeatCategory } from "@types";
import { SidebarSection } from "../sidebar-section/sidebar-section"; import { SidebarSection } from "../sidebar-section/sidebar-section";
import "./sidebar.scss" import "./sidebar.scss";
import "../../../scss/_variables.scss" import "../../../scss/_variables.scss";
const durationTranslation: Record<string, string> = { const durationTranslation: Record<string, string> = {
Hours: "hours", Hours: "hours",

View File

@ -117,158 +117,159 @@ export function Sidebar() {
} }
}, [objectId, shop, gameTitle]); }, [objectId, shop, gameTitle]);
return ( return (
<aside className="sidebar__content"> <aside className="sidebar__content">
{userDetails === null && ( {userDetails === null && (
<SidebarSection title={t("achievements")}> <SidebarSection title={t("achievements")}>
<div className="sidebar__overlay"> <div className="sidebar__overlay">
<LockIcon size={36} /> <LockIcon size={36} />
<h3>{t("sign_in_to_see_achievements")}</h3> <h3>{t("sign_in_to_see_achievements")}</h3>
</div> </div>
<ul className="sidebar__list sidebar__list--blurred"> <ul className="sidebar__list sidebar__list--blurred">
{fakeAchievements.map((achievement, index) => ( {fakeAchievements.map((achievement, index) => (
<li key={index}> <li key={index}>
<div className="sidebar__list-item"> <div className="sidebar__list-item">
<img <img
className={classNames("sidebar__list-item-image", { className={classNames("sidebar__list-item-image", {
"sidebar__list-item-image--unlocked": achievement.unlocked, "sidebar__list-item-image--unlocked":
"sidebar__list-item-image--blurred": true, achievement.unlocked,
})} "sidebar__list-item-image--blurred": true,
src={achievement.icon} })}
alt={achievement.displayName} src={achievement.icon}
/> alt={achievement.displayName}
<div> />
<p>{achievement.displayName}</p> <div>
<small> <p>{achievement.displayName}</p>
{achievement.unlockTime != null && <small>
formatDateTime(achievement.unlockTime)} {achievement.unlockTime != null &&
</small> formatDateTime(achievement.unlockTime)}
</small>
</div>
</div> </div>
</div> </li>
</li> ))}
))} </ul>
</ul> </SidebarSection>
</SidebarSection> )}
)} {userDetails && achievements && achievements.length > 0 && (
{userDetails && achievements && achievements.length > 0 && ( <SidebarSection
<SidebarSection title={t("achievements_count", {
title={t("achievements_count", { unlockedCount: achievements.filter((a) => a.unlocked).length,
unlockedCount: achievements.filter((a) => a.unlocked).length, achievementsCount: achievements.length,
achievementsCount: achievements.length, })}
})} >
> <ul className="sidebar__list">
<ul className="sidebar__list"> {!hasActiveSubscription && (
{!hasActiveSubscription && ( <button
<button className="sidebar__subscription-required-button"
className="sidebar__subscription-required-button" onClick={() => showHydraCloudModal("achievements")}
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 <CloudOfflineIcon size={16} />
className={classNames("achievements__list-item-image", { <span>{t("achievements_not_sync")}</span>
"achievements__list-item-image--unlocked": </button>
achievement.unlocked, )}
{achievements.slice(0, 4).map((achievement, index) => (
<li key={index}>
<Link
to={buildGameAchievementPath({
shop: shop,
objectId: objectId!,
title: gameTitle,
})} })}
src={achievement.icon} className="sidebar__list-item"
alt={achievement.displayName} title={achievement.description}
/> >
<div> <img
<p>{achievement.displayName}</p> className={classNames("achievements__list-item-image", {
<small> "achievements__list-item-image--unlocked":
{achievement.unlockTime != null && achievement.unlocked,
formatDateTime(achievement.unlockTime)} })}
</small> src={achievement.icon}
</div> alt={achievement.displayName}
</Link> />
</li> <div>
))} <p>{achievement.displayName}</p>
<small>
{achievement.unlockTime != null &&
formatDateTime(achievement.unlockTime)}
</small>
</div>
</Link>
</li>
))}
<Link <Link
style={{ textAlign: "center" }} style={{ textAlign: "center" }}
to={buildGameAchievementPath({ to={buildGameAchievementPath({
shop: shop, shop: shop,
objectId: objectId!, objectId: objectId!,
title: gameTitle, title: gameTitle,
})} })}
> >
{t("see_all_achievements")} {t("see_all_achievements")}
</Link> </Link>
</ul> </ul>
</SidebarSection> </SidebarSection>
)} )}
{stats && ( {stats && (
<SidebarSection title={t("stats")}> <SidebarSection title={t("stats")}>
<div className="sidebar__stats-section"> <div className="sidebar__stats-section">
<div className="sidebar__stats-category"> <div className="sidebar__stats-category">
<p className="sidebar__stats-category-title"> <p className="sidebar__stats-category-title">
<DownloadIcon size={18} /> <DownloadIcon size={18} />
{t("download_count")} {t("download_count")}
</p> </p>
<p>{numberFormatter.format(stats?.downloadCount)}</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> </div>
</SidebarSection>
)}
<div className="sidebar__stats-category"> <HowLongToBeatSection
<p className="sidebar__stats-category-title"> howLongToBeatData={howLongToBeat.data}
<PeopleIcon size={18} /> isLoading={howLongToBeat.isLoading}
{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> <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

@ -1,7 +1,7 @@
import { LockIcon } from "@primer/octicons-react"; import { LockIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./locked-profile.scss" import "./locked-profile.scss";
export function LockedProfile() { export function LockedProfile() {
const { t } = useTranslation("user_profile"); const { t } = useTranslation("user_profile");

View File

@ -259,8 +259,7 @@
justify-content: flex-end; justify-content: flex-end;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: background: linear-gradient(0deg, rgba(0, 0, 0 0.7) 20%, transparent 100%);
linear-gradient(0deg, rgba(0,0,0 0.70) 20%, transparent 100%);
padding: 8; padding: 8;
} }
@ -272,12 +271,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4; gap: 4;
padding: 4px, padding: 4px;
} }
&__game-card-stats-container { &__game-card-stats-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8; margin-bottom: 8;
color: $muted-color; color: $muted-color;
overflow: hidden; overflow: hidden;

View File

@ -87,9 +87,7 @@ export function ProfileContent() {
const shouldShowRightContent = hasGames || userProfile.friends.length > 0; const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
return ( return (
<section <section className="profile-content__container">
className="profile-content__container"
>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
{!hasGames && ( {!hasGames && (
<div className="profile-content__no-games"> <div className="profile-content__no-games">

View File

@ -63,7 +63,9 @@ export function RecentGamesBox() {
/> />
<div className="profile-content__list-item-details"> <div className="profile-content__list-item-details">
<span className="profile-content__list-item-title">{game.title}</span> <span className="profile-content__list-item-title">
{game.title}
</span>
<div className="profile-content__list-item-description"> <div className="profile-content__list-item-description">
<ClockIcon /> <ClockIcon />

View File

@ -1,4 +1,4 @@
import "./profile-hero.scss" import "./profile-hero.scss";
import { useCallback, useContext, useMemo, useState } from "react"; import { useCallback, useContext, useMemo, useState } from "react";
import { userProfileContext } from "@renderer/context"; import { userProfileContext } from "@renderer/context";
@ -362,11 +362,7 @@ export function ProfileHero() {
background: backgroundImage ? backgroundImageLayer : heroBackground, background: backgroundImage ? backgroundImageLayer : heroBackground,
}} }}
> >
<div <div className="profile-hero__actions">{profileActions}</div>
className="profile-hero__actions"
>
{profileActions}
</div>
</div> </div>
</section> </section>
</> </>

View File

@ -1,7 +1,7 @@
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 "../../_theme.scss" import "../../_theme.scss";
import "./profile.scss"; import "./profile.scss";
import { UserProfileContextProvider } from "@renderer/context"; import { UserProfileContextProvider } from "@renderer/context";

View File

@ -75,9 +75,7 @@ export function ReportProfile() {
title={t("report_profile")} title={t("report_profile")}
clickOutsideToClose={false} clickOutsideToClose={false}
> >
<form <form className="report-profile__report-modal">
className="report-profile__report-modal"
>
<Controller <Controller
control={control} control={control}
name="reason" name="reason"

View File

@ -1,23 +1,23 @@
@import "../../scss/variables"; @import "../../scss/variables";
.download-source { .download-source {
&__add-source { &__add-source {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: #{$spacing-unit}; gap: #{$spacing-unit};
min-width: 500px, min-width: 500px;
} }
&__validation-result { &__validation-result {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: #{$spacing-unit * 3}; margin-top: #{$spacing-unit * 3};
} }
&__input { &__input {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: #{$spacing-unit / 2}; gap: #{$spacing-unit / 2};
} }
} }

View File

@ -137,9 +137,7 @@ export function AddDownloadSourceModal({
description={t("add_download_source_description")} description={t("add_download_source_description")}
onClose={onClose} onClose={onClose}
> >
<div <div className="download-source__add-source">
className="download-source__add-source"
>
<TextField <TextField
{...register("url")} {...register("url")}
label={t("download_source_url")} label={t("download_source_url")}
@ -159,12 +157,8 @@ export function AddDownloadSourceModal({
/> />
{validationResult && ( {validationResult && (
<div <div className="download-source__validation-result">
className="download-source__validation-result" <div className="download-source__input">
>
<div
className="download-source__input"
>
<h4>{validationResult?.name}</h4> <h4>{validationResult?.name}</h4>
<small> <small>
{t("found_download_option", { {t("found_download_option", {

View File

@ -12,7 +12,7 @@
} }
&__field-spacing { &__field-spacing {
margin-top: #{$spacing-unit}; margin-top: #{$spacing-unit};
} }
&__submit-button { &__submit-button {

View File

@ -6,7 +6,7 @@ import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import { useAppSelector, useToast } from "@renderer/hooks"; import { useAppSelector, useToast } from "@renderer/hooks";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import "./settings-real-debrid.scss" 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,7 +78,9 @@ export function SettingsRealDebrid() {
return ( return (
<form className="settings-real-debrid__form" onSubmit={handleFormSubmit}> <form className="settings-real-debrid__form" onSubmit={handleFormSubmit}>
<p className="settings-real-debrid__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")}

View File

@ -1,6 +1,6 @@
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import "./settings.scss"; 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";

View File

@ -21,10 +21,7 @@ export const HydraCloudModal = ({
return ( return (
<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} className="hydra-cloud__on-close">
data-hydra-cloud-feature={feature}
className="hydra-cloud__on-close"
>
{t("hydra_cloud_feature_found")} {t("hydra_cloud_feature_found")}
<Button onClick={handleClickOpenCheckout}>{t("learn_more")}</Button> <Button onClick={handleClickOpenCheckout}>{t("learn_more")}</Button>
</div> </div>

View File

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

View File

@ -105,7 +105,10 @@ export const UserFriendItem = (props: UserFriendItemProps) => {
if (type === "BLOCKED") { if (type === "BLOCKED") {
return ( return (
<div className="user-friend-modal__friend-list-container"> <div className="user-friend-modal__friend-list-container">
<div className="user-friend-modal__friend-list-button" 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

View File

@ -127,4 +127,4 @@ export const UserFriendModalAddFriend = ({
</div> </div>
</> </>
); );
}; };

View File

@ -103,8 +103,11 @@ export const UserFriendModalList = ({
overflowY: "scroll", overflowY: "scroll",
}} }}
> >
{!isLoading && friends.length === 0 && {!isLoading && friends.length === 0 && (
<p className="user-friend-modal__friend-list-display-name">{t("no_friends_added")}</p>} <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

@ -44,7 +44,7 @@
position: "absolute"; position: "absolute";
right: "8px"; right: "8px";
display: "flex"; display: "flex";
gap: #{$spacing-unit} gap: #{$spacing-unit};
} }
&__friend-request-item { &__friend-request-item {
@ -89,7 +89,7 @@
color: $muted-color; color: $muted-color;
} }
} }
&__add-friend-controls { &__add-friend-controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;

View File

@ -38,6 +38,6 @@ $small-font-size: 12px;
$app-container: app-container; $app-container: app-container;
:root { :root {
--background-color: #{$background-color}; --background-color: #{$background-color};
--spacing-unit: #{$spacing-unit}; --spacing-unit: #{$spacing-unit};
} }