mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
full migration to scss
This commit is contained in:
parent
b0eb7c16cd
commit
d038398750
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
|
@ -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[];
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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 />
|
||||||
) : (
|
) : (
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
|
@ -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";
|
||||||
|
@ -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"> {}
|
||||||
|
@ -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") => {
|
||||||
|
@ -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) {
|
||||||
|
@ -24,7 +24,6 @@ $hero-height: 300px;
|
|||||||
|
|
||||||
&__blurred-content {
|
&__blurred-content {
|
||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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";
|
||||||
|
@ -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"
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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", {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__field-spacing {
|
&__field-spacing {
|
||||||
margin-top: #{$spacing-unit};
|
margin-top: #{$spacing-unit};
|
||||||
}
|
}
|
||||||
|
|
||||||
&__submit-button {
|
&__submit-button {
|
||||||
|
@ -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")}
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -127,4 +127,4 @@ export const UserFriendModalAddFriend = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user