mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-12 11:12:07 +00:00
refactor: migrate game details styles from VE to SCSS + BEM
This commit is contained in:
parent
296524f894
commit
a52979d912
31 changed files with 1197 additions and 264 deletions
|
@ -0,0 +1,41 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.cloud-sync-files-modal {
|
||||
&__mapping-methods {
|
||||
display: grid;
|
||||
gap: globals.$spacing-unit;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&__file-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
margin-top: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__file-item {
|
||||
flex: 1;
|
||||
color: globals.$muted-color;
|
||||
text-decoration: underline;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__mapping-label {
|
||||
margin-bottom: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__custom-path {
|
||||
margin-top: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./cloud-sync-files-modal.css";
|
||||
import "./cloud-sync-files-modal.scss";
|
||||
import { formatBytes } from "@shared";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
@ -96,10 +96,12 @@ export function CloudSyncFilesModal({
|
|||
description={t("manage_files_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
||||
<div className="cloud-sync-files-modal__container">
|
||||
<span className="cloud-sync-files-modal__mapping-label">
|
||||
{t("mapping_method_label")}
|
||||
</span>
|
||||
|
||||
<div className={styles.mappingMethods}>
|
||||
<div className="cloud-sync-files-modal__mapping-methods">
|
||||
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
||||
<Button
|
||||
key={mappingMethod}
|
||||
|
@ -119,7 +121,7 @@ export function CloudSyncFilesModal({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div className="cloud-sync-files-modal__custom-path">
|
||||
{selectedFileMappingMethod === FileMappingMethod.Automatic ? (
|
||||
<p>{t("files_automatically_mapped")}</p>
|
||||
) : (
|
||||
|
@ -142,11 +144,11 @@ export function CloudSyncFilesModal({
|
|||
/>
|
||||
)}
|
||||
|
||||
<ul className={styles.fileList}>
|
||||
<ul className="cloud-sync-files-modal__file-list">
|
||||
{files.map((file) => (
|
||||
<li key={file.path} style={{ display: "flex" }}>
|
||||
<li key={file.path} className="cloud-sync-files-modal__file-item">
|
||||
<button
|
||||
className={styles.fileItem}
|
||||
className="cloud-sync-files-modal__file-item"
|
||||
onClick={() => window.electron.showItemInFolder(file.path)}
|
||||
>
|
||||
{file.path.split("/").at(-1)}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-sync-modal {
|
||||
&__header {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title-container {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__backups-header {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__artifacts {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__artifact {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
background-color: globals.$dark-background-color;
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
justify-content: space-between;
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__sync-icon {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: globals.$muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__manage-files-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-self: flex-start;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: globals.$body-color;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: globals.$disabled-opacity;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Button, Modal, ModalProps } from "@renderer/components";
|
|||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
|
||||
import * as styles from "./cloud-sync-modal.css";
|
||||
import "./cloud-sync-modal.scss";
|
||||
import { formatBytes } from "@shared";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
|
@ -18,7 +18,6 @@ import { useAppSelector, useToast } from "@renderer/hooks";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { AxiosProgressEvent } from "axios";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface CloudSyncModalProps
|
||||
extends Omit<ModalProps, "children" | "title"> {}
|
||||
|
@ -95,7 +94,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
if (uploadingBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
{t("uploading_backup")}
|
||||
</span>
|
||||
);
|
||||
|
@ -104,7 +103,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
if (restoringBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
{t("restoring_backup", {
|
||||
progress: formatDownloadProgress(
|
||||
backupDownloadProgress?.progress ?? 0
|
||||
|
@ -117,7 +116,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
if (loadingPreview) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
{t("loading_save_preview")}
|
||||
</span>
|
||||
);
|
||||
|
@ -157,21 +156,14 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
onClose={onClose}
|
||||
large
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||
<div className="cloud-sync-modal__header">
|
||||
<div className="cloud-sync-modal__title-container">
|
||||
<h2>{gameTitle}</h2>
|
||||
<p>{backupStateLabel}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.manageFilesButton}
|
||||
className="cloud-sync-modal__manage-files-button"
|
||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||
disabled={disableActions}
|
||||
>
|
||||
|
@ -188,40 +180,36 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
artifacts.length >= backupsPerGameLimit
|
||||
}
|
||||
>
|
||||
{uploadingBackup ? (
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
) : (
|
||||
<UploadIcon />
|
||||
)}
|
||||
{t("create_backup")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: SPACING_UNIT,
|
||||
}}
|
||||
>
|
||||
{uploadingBackup && (
|
||||
<progress
|
||||
className="cloud-sync-modal__progress"
|
||||
value={backupDownloadProgress?.progress ?? 0}
|
||||
max={100}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="cloud-sync-modal__backups-header">
|
||||
<h2>{t("backups")}</h2>
|
||||
<small>
|
||||
{artifacts.length} / {backupsPerGameLimit}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{artifacts.length > 0 ? (
|
||||
<ul className={styles.artifacts}>
|
||||
<ul className="cloud-sync-modal__artifacts">
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.id} className={styles.artifactButton}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<li key={artifact.id} className="cloud-sync-modal__artifact">
|
||||
<div className="cloud-sync-modal__artifact-info">
|
||||
<div className="cloud-sync-modal__artifact-header">
|
||||
<h3>
|
||||
{t("backup_from", {
|
||||
date: format(artifact.createdAt, "dd/MM/yyyy"),
|
||||
|
@ -230,29 +218,33 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
||||
</div>
|
||||
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
<DeviceDesktopIcon size={14} />
|
||||
{artifact.hostname}
|
||||
</span>
|
||||
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
<InfoIcon size={14} />
|
||||
{artifact.downloadOptionTitle ?? t("no_download_option_info")}
|
||||
</span>
|
||||
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span className="cloud-sync-modal__artifact-meta">
|
||||
<ClockIcon size={14} />
|
||||
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<div className="cloud-sync-modal__artifact-actions">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleBackupInstallClick(artifact.id)}
|
||||
disabled={disableActions}
|
||||
>
|
||||
{restoringBackup ? (
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
) : (
|
||||
<HistoryIcon />
|
||||
)}
|
||||
{t("install_backup")}
|
||||
</Button>
|
||||
<Button
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.description-header {
|
||||
width: 100%;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: globals.$background-color;
|
||||
height: 72px;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
|
@ -1,19 +1,17 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./description-header.css";
|
||||
import { useContext } from "react";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import "./description-header.scss";
|
||||
|
||||
export function DescriptionHeader() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
if (!shopDetails) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.descriptionHeader}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<div className="description-header">
|
||||
<section className="description-header__info">
|
||||
<p>
|
||||
{t("release_date", {
|
||||
date: shopDetails?.release_date.date,
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.gallery-slider {
|
||||
&__container {
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
transition: translate 0.3s ease-in-out;
|
||||
border-radius: 4px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&__animation-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
width: 100%;
|
||||
padding: globals.$spacing-unit 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview-button {
|
||||
cursor: pointer;
|
||||
width: 20%;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
opacity: 0.3;
|
||||
transition:
|
||||
translate 0.3s ease-in-out,
|
||||
opacity 0.2s ease;
|
||||
border-radius: 4px;
|
||||
border: solid 1px globals.$border-color;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview-image {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button {
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
transition: all 0.2s ease-in-out;
|
||||
border-radius: 50%;
|
||||
color: globals.$muted-color;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&--left {
|
||||
left: 0;
|
||||
margin-left: globals.$spacing-unit;
|
||||
transform: translateX(calc(-1 * (48px + globals.$spacing-unit)));
|
||||
|
||||
&.gallery-slider__button--visible {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
right: 0;
|
||||
margin-right: globals.$spacing-unit;
|
||||
transform: translateX(calc(48px + globals.$spacing-unit));
|
||||
|
||||
&.gallery-slider__button--visible {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./gallery-slider.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import "./gallery-slider.scss";
|
||||
|
||||
export function GallerySlider() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
@ -97,11 +96,11 @@ export function GallerySlider() {
|
|||
return (
|
||||
<>
|
||||
{hasScreenshots && (
|
||||
<div className={styles.gallerySliderContainer}>
|
||||
<div className="gallery-slider__container">
|
||||
<div
|
||||
onMouseEnter={() => setShowArrows(true)}
|
||||
onMouseLeave={() => setShowArrows(false)}
|
||||
className={styles.gallerySliderAnimationContainer}
|
||||
className="gallery-slider__animation-container"
|
||||
ref={mediaContainerRef}
|
||||
>
|
||||
{shopDetails.movies &&
|
||||
|
@ -109,7 +108,7 @@ export function GallerySlider() {
|
|||
<video
|
||||
key={video.id}
|
||||
controls
|
||||
className={styles.gallerySliderMedia}
|
||||
className="gallery-slider__media"
|
||||
poster={video.thumbnail}
|
||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||
loop
|
||||
|
@ -124,7 +123,7 @@ export function GallerySlider() {
|
|||
shopDetails.screenshots?.map((image, i) => (
|
||||
<img
|
||||
key={image.id}
|
||||
className={styles.gallerySliderMedia}
|
||||
className="gallery-slider__media"
|
||||
src={image.path_full}
|
||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||
alt={t("screenshot", { number: i + 1 })}
|
||||
|
@ -135,10 +134,11 @@ export function GallerySlider() {
|
|||
<button
|
||||
onClick={showPrevImage}
|
||||
type="button"
|
||||
className={styles.gallerySliderButton({
|
||||
visible: showArrows,
|
||||
direction: "left",
|
||||
})}
|
||||
className={`gallery-slider__button gallery-slider__button--left ${
|
||||
showArrows
|
||||
? "gallery-slider__button--visible"
|
||||
: "gallery-slider__button--hidden"
|
||||
}`}
|
||||
aria-label={t("previous_screenshot")}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
@ -148,10 +148,11 @@ export function GallerySlider() {
|
|||
<button
|
||||
onClick={showNextImage}
|
||||
type="button"
|
||||
className={styles.gallerySliderButton({
|
||||
visible: showArrows,
|
||||
direction: "right",
|
||||
})}
|
||||
className={`gallery-slider__button gallery-slider__button--right ${
|
||||
showArrows
|
||||
? "gallery-slider__button--visible"
|
||||
: "gallery-slider__button--hidden"
|
||||
}`}
|
||||
aria-label={t("next_screenshot")}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
@ -159,20 +160,22 @@ export function GallerySlider() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
|
||||
<div className="gallery-slider__preview" ref={scrollContainerRef}>
|
||||
{previews.map((media, i) => (
|
||||
<button
|
||||
key={media.id}
|
||||
type="button"
|
||||
className={styles.mediaPreviewButton({
|
||||
active: mediaIndex === i,
|
||||
})}
|
||||
className={`gallery-slider__preview-button ${
|
||||
mediaIndex === i
|
||||
? "gallery-slider__preview-button--active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setMediaIndex(i)}
|
||||
aria-label={t("open_screenshot", { number: i + 1 })}
|
||||
>
|
||||
<img
|
||||
src={media.thumbnail}
|
||||
className={styles.mediaPreview}
|
||||
className="gallery-slider__preview-image"
|
||||
alt={t("screenshot", { number: i + 1 })}
|
||||
/>
|
||||
</button>
|
||||
|
|
|
@ -7,7 +7,6 @@ import { DescriptionHeader } from "./description-header/description-header";
|
|||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { AuthPage, steamUrlBuilder } from "@shared";
|
||||
|
@ -15,7 +14,9 @@ import { AuthPage, steamUrlBuilder } from "@shared";
|
|||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./game-details.scss";
|
||||
|
||||
const HERO_HEIGHT = 300;
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
|
||||
export function GameDetailsContent() {
|
||||
|
@ -80,7 +81,7 @@ export function GameDetailsContent() {
|
|||
}, [objectId]);
|
||||
|
||||
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 opacity = Math.max(
|
||||
|
@ -118,10 +119,12 @@ export function GameDetailsContent() {
|
|||
}, [getGameArtifacts]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
||||
<div
|
||||
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
||||
className={styles.heroImage}
|
||||
className="game-details__hero-image"
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
|
@ -129,47 +132,38 @@ export function GameDetailsContent() {
|
|||
<section
|
||||
ref={containerRef}
|
||||
onScroll={onScroll}
|
||||
className={styles.container}
|
||||
className="game-details__container"
|
||||
>
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div ref={heroRef} className="game-details__hero">
|
||||
<div
|
||||
className="game-details__hero-backdrop"
|
||||
style={{
|
||||
backgroundColor: gameColor,
|
||||
flex: 1,
|
||||
opacity: Math.min(1, 1 - backdropOpactiy),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.heroLogoBackdrop}
|
||||
className="game-details__hero-logo-backdrop"
|
||||
style={{ opacity: backdropOpactiy }}
|
||||
>
|
||||
<div className={styles.heroContent}>
|
||||
<div className="game-details__hero-content">
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectId!)}
|
||||
className={styles.gameLogo}
|
||||
className="game-details__game-logo"
|
||||
alt={game?.title}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cloudSyncButton}
|
||||
className="game-details__cloud-sync-button"
|
||||
onClick={handleCloudSaveButtonClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16 + 4,
|
||||
height: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div className="game-details__cloud-icon-container">
|
||||
<img
|
||||
src={cloudIconAnimated}
|
||||
alt="Cloud icon"
|
||||
style={{ width: 26, position: "absolute", top: -3 }}
|
||||
className="game-details__cloud-icon"
|
||||
/>
|
||||
</div>
|
||||
{t("cloud_save")}
|
||||
|
@ -180,8 +174,8 @@ export function GameDetailsContent() {
|
|||
|
||||
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
||||
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<div className="game-details__description-container">
|
||||
<div className="game-details__description-content">
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
|
@ -189,7 +183,7 @@ export function GameDetailsContent() {
|
|||
dangerouslySetInnerHTML={{
|
||||
__html: aboutTheGame,
|
||||
}}
|
||||
className={styles.description}
|
||||
className="game-details__description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,65 +1,52 @@
|
|||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import * as sidebarStyles from "./sidebar/sidebar.css";
|
||||
import * as descriptionHeaderStyles from "./description-header/description-header.css";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./game-details.scss";
|
||||
|
||||
export function GameDetailsSkeleton() {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
<div className="game-details__container">
|
||||
<div className="game-details__hero">
|
||||
<Skeleton className="game-details__hero-image-skeleton" />
|
||||
</div>
|
||||
<div className={styles.heroPanelSkeleton}>
|
||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
||||
<div className="game-details__hero-panel-skeleton">
|
||||
<section className="description-header__info">
|
||||
<Skeleton width={155} />
|
||||
<Skeleton width={135} />
|
||||
</section>
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<div className={descriptionHeaderStyles.descriptionHeader}>
|
||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
||||
<div className="game-details__description-container">
|
||||
<div className="game-details__description-content">
|
||||
<div className="description-header">
|
||||
<section className="description-header__info">
|
||||
<Skeleton width={145} />
|
||||
<Skeleton width={150} />
|
||||
</section>
|
||||
</div>
|
||||
<div className={styles.descriptionSkeleton}>
|
||||
<div className="game-details__description-skeleton">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton key={index} />
|
||||
))}
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
<Skeleton className="game-details__hero-image-skeleton" />
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Skeleton key={index} />
|
||||
))}
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
<Skeleton className="game-details__hero-image-skeleton" />
|
||||
<Skeleton />
|
||||
</div>
|
||||
</div>
|
||||
<div className={sidebarStyles.contentSidebar}>
|
||||
<div className={sidebarStyles.requirementButtonContainer}>
|
||||
<Button
|
||||
className={sidebarStyles.requirementButton}
|
||||
theme="primary"
|
||||
disabled
|
||||
>
|
||||
<div className="content-sidebar">
|
||||
<div className="requirement__button-container">
|
||||
<Button className="requirement__button" theme="primary" disabled>
|
||||
{t("minimum")}
|
||||
</Button>
|
||||
<Button
|
||||
className={sidebarStyles.requirementButton}
|
||||
theme="outline"
|
||||
disabled
|
||||
>
|
||||
<Button className="requirement__button" theme="outline" disabled>
|
||||
{t("recommended")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={sidebarStyles.requirementsDetailsSkeleton}>
|
||||
<div className="requirement__details-skeleton">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<Skeleton key={index} height={20} />
|
||||
))}
|
||||
|
|
270
src/renderer/src/pages/game-details/game-details.scss
Normal file
270
src/renderer/src/pages/game-details/game-details.scss
Normal file
|
@ -0,0 +1,270 @@
|
|||
@use "../../scss/globals.scss";
|
||||
|
||||
$hero-height: 300px;
|
||||
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
transform: translateY(calc(40px + globals.$spacing-unit * 2));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.game-details {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
&--blurred {
|
||||
filter: blur(20px);
|
||||
}
|
||||
}
|
||||
|
||||
&__hero {
|
||||
width: 100%;
|
||||
height: $hero-height;
|
||||
min-height: $hero-height;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
height: 350px;
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
&__hero-content {
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__hero-logo-backdrop {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%);
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__hero-image {
|
||||
width: 100%;
|
||||
height: $hero-height;
|
||||
min-height: $hero-height;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
transition: all ease 0.2s;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
object-position: center;
|
||||
height: 350px;
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
&__game-logo {
|
||||
width: 300px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&__hero-image-skeleton {
|
||||
height: 300px;
|
||||
|
||||
@media (min-width: 1250px) {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__description-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
globals.$background-color 50%,
|
||||
globals.$dark-background-color 100%
|
||||
);
|
||||
}
|
||||
|
||||
&__description-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__description {
|
||||
user-select: text;
|
||||
line-height: 22px;
|
||||
font-size: globals.$body-font-size;
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 5px;
|
||||
margin-top: globals.$spacing-unit;
|
||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
a {
|
||||
color: globals.$body-color;
|
||||
}
|
||||
|
||||
.bb_tag {
|
||||
margin-top: calc(globals.$spacing-unit * 2);
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__description-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 60%;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&__randomizer-button {
|
||||
animation: slide-in 0.2s;
|
||||
position: fixed;
|
||||
bottom: calc(globals.$spacing-unit * 3);
|
||||
right: calc(9px + globals.$spacing-unit * 2);
|
||||
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 10px 1px;
|
||||
border: solid 2px globals.$border-color;
|
||||
z-index: 1;
|
||||
background-color: globals.$background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: globals.$background-color;
|
||||
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 15px 5px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
opacity: 0.8;
|
||||
background-color: globals.$background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__hero-panel-skeleton {
|
||||
width: 100%;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: globals.$background-color;
|
||||
height: 72px;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
&__cloud-sync-button {
|
||||
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 8px;
|
||||
transition: all ease 0.2s;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: globals.$spacing-unit;
|
||||
color: globals.$muted-color;
|
||||
font-size: globals.$small-font-size;
|
||||
border: solid 1px globals.$border-color;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
|
||||
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: globals.$disabled-opacity;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__stars-icon-container {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__stars-icon {
|
||||
width: 70px;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: -27px;
|
||||
}
|
||||
|
||||
&__cloud-icon-container {
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__cloud-icon {
|
||||
width: 26px;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
&__hero-backdrop {
|
||||
flex: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
|
@ -11,7 +11,6 @@ import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||
import * as styles from "./game-details.css";
|
||||
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
|
@ -27,6 +26,7 @@ import { GameOptionsModal, RepacksModal } from "./modals";
|
|||
import { Downloader, getDownloadersForUri } from "@shared";
|
||||
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
|
||||
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
|
||||
import "./game-details.scss";
|
||||
|
||||
export default function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
|
@ -185,23 +185,16 @@ export default function GameDetails() {
|
|||
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
className="game-details__randomizer-button"
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame || randomizerLocked}
|
||||
>
|
||||
<div
|
||||
style={{ width: 16, height: 16, position: "relative" }}
|
||||
>
|
||||
<div className="game-details__stars-icon-container">
|
||||
<img
|
||||
src={starsIconAnimated}
|
||||
alt="Stars animation"
|
||||
style={{
|
||||
width: 70,
|
||||
position: "absolute",
|
||||
top: -28,
|
||||
left: -27,
|
||||
}}
|
||||
className="game-details__stars-icon"
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.hero-panel-actions {
|
||||
&__action {
|
||||
border: solid 1px globals.$muted-color;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__separator {
|
||||
width: 1px;
|
||||
background-color: globals.$muted-color;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
|
@ -8,9 +8,8 @@ import { Button } from "@renderer/components";
|
|||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import "./hero-panel-actions.scss";
|
||||
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
|
@ -84,7 +83,7 @@ export function HeroPanelActions() {
|
|||
theme="outline"
|
||||
disabled={toggleLibraryGameDisabled}
|
||||
onClick={addGameToLibrary}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
{t("add_to_library")}
|
||||
|
@ -96,7 +95,7 @@ export function HeroPanelActions() {
|
|||
onClick={() => setShowRepacksModal(true)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
|
@ -109,7 +108,7 @@ export function HeroPanelActions() {
|
|||
onClick={closeGame}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
{t("close")}
|
||||
</Button>
|
||||
|
@ -122,7 +121,7 @@ export function HeroPanelActions() {
|
|||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
<PlayIcon />
|
||||
{t("play")}
|
||||
|
@ -135,7 +134,7 @@ export function HeroPanelActions() {
|
|||
onClick={() => setShowRepacksModal(true)}
|
||||
theme="outline"
|
||||
disabled={isGameDownloading || !repacks.length}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{t("download")}
|
||||
|
@ -154,16 +153,14 @@ export function HeroPanelActions() {
|
|||
|
||||
if (game) {
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<div className="hero-panel-actions__container">
|
||||
{gameActionButton()}
|
||||
|
||||
<div className={styles.separator} />
|
||||
|
||||
<div className="hero-panel-actions__separator" />
|
||||
<Button
|
||||
onClick={() => setShowGameOptionsModal(true)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
className="hero-panel-actions__action"
|
||||
>
|
||||
<GearIcon />
|
||||
{t("options")}
|
||||
|
|
66
src/renderer/src/pages/game-details/hero/hero-panel.scss
Normal file
66
src/renderer/src/pages/game-details/hero/hero-panel.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.hero-panel {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
min-height: 72px;
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||
background-color: globals.$dark-background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: all ease 0.2s;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
position: sticky;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
&--stuck {
|
||||
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__download-details {
|
||||
gap: globals.$spacing-unit;
|
||||
display: flex;
|
||||
color: globals.$body-color;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__downloads-link {
|
||||
color: globals.$body-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: globals.$disabled-opacity;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
|
|||
import { useDate, useDownload } from "@renderer/hooks";
|
||||
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import "./hero-panel.scss";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
isHeaderStuck: boolean;
|
||||
|
@ -54,28 +54,26 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|||
game?.status === "paused";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{ backgroundColor: gameColor }}
|
||||
className={styles.panel({ stuck: isHeaderStuck })}
|
||||
className={`hero-panel ${isHeaderStuck ? "hero-panel--stuck" : ""}`}
|
||||
>
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>
|
||||
<div className="hero-panel__content">{getInfo()}</div>
|
||||
<div className="hero-panel__actions">
|
||||
<HeroPanelActions />
|
||||
</div>
|
||||
|
||||
{showProgressBar && (
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
||||
}
|
||||
className={styles.progressBar({
|
||||
disabled: game?.status === "paused",
|
||||
})}
|
||||
value={isGameDownloading ? lastPacket?.game.progress : game?.progress}
|
||||
className={`hero-panel__progress-bar ${
|
||||
game?.status === "paused"
|
||||
? "hero-panel__progress-bar--disabled"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.download-settings-modal {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__downloads-path-field {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__hint-text {
|
||||
font-size: globals.$small-font-size;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
|
||||
&__downloaders {
|
||||
display: grid;
|
||||
gap: globals.$spacing-unit;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&__downloader-option {
|
||||
position: relative;
|
||||
|
||||
&:only-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
&__downloader-icon {
|
||||
position: absolute;
|
||||
left: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__path-error {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./download-settings-modal.css";
|
||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
|
||||
import "./download-settings-modal.scss";
|
||||
|
||||
export interface DownloadSettingsModalProps {
|
||||
visible: boolean;
|
||||
|
@ -145,21 +142,15 @@ export function DownloadSettingsModal({
|
|||
})}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="download-settings-modal__container">
|
||||
<div className="download-settings-modal__downloads-path-field">
|
||||
<span>{t("downloader")}</span>
|
||||
|
||||
<div className={styles.downloaders}>
|
||||
<div className="download-settings-modal__downloaders">
|
||||
{downloaders.map((downloader) => (
|
||||
<Button
|
||||
key={downloader}
|
||||
className={styles.downloaderOption}
|
||||
className="download-settings-modal__downloader-option"
|
||||
theme={
|
||||
selectedDownloader === downloader ? "primary" : "outline"
|
||||
}
|
||||
|
@ -170,7 +161,7 @@ export function DownloadSettingsModal({
|
|||
onClick={() => setSelectedDownloader(downloader)}
|
||||
>
|
||||
{selectedDownloader === downloader && (
|
||||
<CheckCircleFillIcon className={styles.downloaderIcon} />
|
||||
<CheckCircleFillIcon className="download-settings-modal__downloader-icon" />
|
||||
)}
|
||||
{DOWNLOADER_NAME[downloader]}
|
||||
</Button>
|
||||
|
@ -178,13 +169,7 @@ export function DownloadSettingsModal({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="download-settings-modal__downloads-path-field">
|
||||
<TextField
|
||||
value={selectedPath}
|
||||
readOnly
|
||||
|
@ -193,7 +178,7 @@ export function DownloadSettingsModal({
|
|||
error={
|
||||
hasWritePermission === false ? (
|
||||
<span
|
||||
className={styles.pathError}
|
||||
className="download-settings-modal__path-error"
|
||||
data-open-article="cannot-write-directory"
|
||||
>
|
||||
{t("no_write_permission")}
|
||||
|
@ -212,7 +197,7 @@ export function DownloadSettingsModal({
|
|||
}
|
||||
/>
|
||||
|
||||
<p className={styles.hintText}>
|
||||
<p className="download-settings-modal__hint-text">
|
||||
<Trans i18nKey="select_folder_hint" ns="game_details">
|
||||
<Link to="/settings" />
|
||||
</Trans>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.game-options-modal {
|
||||
&__container {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__header-description {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
|||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||
import { debounce } from "lodash-es";
|
||||
import "./game-options-modal.scss";
|
||||
|
||||
export interface GameOptionsModalProps {
|
||||
visible: boolean;
|
||||
|
@ -199,10 +200,10 @@ export function GameOptionsModal({
|
|||
onClose={onClose}
|
||||
large={true}
|
||||
>
|
||||
<div className={styles.optionsContainer}>
|
||||
<div className={styles.gameOptionHeader}>
|
||||
<div className="game-options-modal__container">
|
||||
<div className="game-options-modal__header">
|
||||
<h2>{t("executable_section_title")}</h2>
|
||||
<h4 className={styles.gameOptionHeaderDescription}>
|
||||
<h4 className="game-options-modal__header-description">
|
||||
{t("executable_section_description")}
|
||||
</h4>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.remove-from-library-modal {
|
||||
&__actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./remove-from-library-modal.css";
|
||||
import type { Game } from "@types";
|
||||
import "./remove-from-library-modal.scss";
|
||||
|
||||
interface RemoveGameFromLibraryModalProps {
|
||||
visible: boolean;
|
||||
|
@ -30,7 +30,7 @@ export function RemoveGameFromLibraryModal({
|
|||
description={t("remove_from_library_description", { game: game.title })}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.deleteActionsButtonsCtn}>
|
||||
<div className="remove-from-library-modal__actions">
|
||||
<Button onClick={handleRemoveGame} theme="outline">
|
||||
{t("remove")}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.repacks-modal {
|
||||
&__repacks {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__repack-button {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: globals.$spacing-unit;
|
||||
color: globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__repack-title {
|
||||
color: globals.$muted-color;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__repack-info {
|
||||
font-size: globals.$small-font-size;
|
||||
}
|
||||
}
|
|
@ -4,14 +4,13 @@ import { useTranslation } from "react-i18next";
|
|||
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
||||
import type { GameRepack } from "@types";
|
||||
|
||||
import * as styles from "./repacks-modal.css";
|
||||
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { Downloader } from "@shared";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import "./repacks-modal.scss";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
visible: boolean;
|
||||
|
@ -90,7 +89,7 @@ export function RepacksModal({
|
|||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
||||
</div>
|
||||
|
||||
<div className={styles.repacks}>
|
||||
<div className="repacks-modal__repacks">
|
||||
{filteredRepacks.map((repack) => {
|
||||
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
||||
|
||||
|
@ -99,17 +98,15 @@ export function RepacksModal({
|
|||
key={repack.id}
|
||||
theme="dark"
|
||||
onClick={() => handleRepackClick(repack)}
|
||||
className={styles.repackButton}
|
||||
className="repacks-modal__repack-button"
|
||||
>
|
||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||
{repack.title}
|
||||
</p>
|
||||
<p className="repacks-modal__repack-title">{repack.title}</p>
|
||||
|
||||
{isLastDownloadedOption && (
|
||||
<Badge>{t("last_downloaded_option")}</Badge>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: "12px" }}>
|
||||
<p className="repacks-modal__repack-info">
|
||||
{repack.fileSize} - {repack.repacker} -{" "}
|
||||
{repack.uploadDate ? formatDate(repack.uploadDate!) : ""}
|
||||
</p>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.reset-achievements-modal {
|
||||
&__actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import * as styles from "./remove-from-library-modal.css";
|
||||
import type { Game } from "@types";
|
||||
import "./reset-achievements-modal.scss";
|
||||
|
||||
type ResetAchievementsModalProps = Readonly<{
|
||||
visible: boolean;
|
||||
game: Game;
|
||||
|
@ -34,7 +35,7 @@ export function ResetAchievementsModal({
|
|||
game: game.title,
|
||||
})}
|
||||
>
|
||||
<div className={styles.deleteActionsButtonsCtn}>
|
||||
<div className="reset-achievements-modal__actions">
|
||||
<Button onClick={handleResetAchievements} theme="outline">
|
||||
{t("reset_achievements")}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.sidebar-section {
|
||||
&__button {
|
||||
height: 72px;
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: globals.$background-color;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.2s;
|
||||
gap: globals.$spacing-unit;
|
||||
font-size: globals.$body-font-size;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: globals.$active-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
transition: transform ease 0.2s;
|
||||
|
||||
&--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
|
||||
position: relative;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import * as styles from "./sidebar-section.css";
|
||||
import "./sidebar-section.scss";
|
||||
|
||||
export interface SidebarSectionProps {
|
||||
title: string;
|
||||
|
@ -22,23 +21,25 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
|
|||
}, [isOpen, children, height]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="sidebar-section">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={styles.sidebarSectionButton}
|
||||
className="sidebar-section__button"
|
||||
>
|
||||
<ChevronDownIcon className={styles.chevron({ open: isOpen })} />
|
||||
<ChevronDownIcon
|
||||
className={`sidebar-section__chevron ${
|
||||
isOpen ? "sidebar-section__chevron--open" : ""
|
||||
}`}
|
||||
/>
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={content}
|
||||
className="sidebar-section__content"
|
||||
style={{
|
||||
maxHeight: `${height}px`,
|
||||
overflow: "hidden",
|
||||
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -2,9 +2,8 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import "./sidebar.scss";
|
||||
|
||||
const durationTranslation: Record<string, string> = {
|
||||
Hours: "hours",
|
||||
|
@ -32,15 +31,12 @@ export function HowLongToBeatSection({
|
|||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<SidebarSection title="HowLongToBeat">
|
||||
<ul className={styles.howLongToBeatCategoriesList}>
|
||||
<ul className="how-long-to-beat__categories-list">
|
||||
{howLongToBeatData
|
||||
? howLongToBeatData.map((category) => (
|
||||
<li
|
||||
key={category.title}
|
||||
className={styles.howLongToBeatCategory}
|
||||
>
|
||||
<li key={category.title} className="how-long-to-beat__category">
|
||||
<p
|
||||
className={styles.howLongToBeatCategoryLabel}
|
||||
className="how-long-to-beat__category-label"
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
|
@ -48,7 +44,7 @@ export function HowLongToBeatSection({
|
|||
{category.title}
|
||||
</p>
|
||||
|
||||
<p className={styles.howLongToBeatCategoryLabel}>
|
||||
<p className="how-long-to-beat__category-label">
|
||||
{getDuration(category.duration)}
|
||||
</p>
|
||||
|
||||
|
@ -62,7 +58,7 @@ export function HowLongToBeatSection({
|
|||
: Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={styles.howLongToBeatCategorySkeleton}
|
||||
className="how-long-to-beat__category-skeleton"
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
|
174
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file
174
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file
|
@ -0,0 +1,174 @@
|
|||
@use "../../../scss/globals.scss";
|
||||
|
||||
.content-sidebar {
|
||||
border-left: solid 1px globals.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement {
|
||||
&__button-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__details {
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
line-height: 22px;
|
||||
font-size: globals.$body-font-size;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.how-long-to-beat {
|
||||
&__categories-list {
|
||||
margin: 0;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 20%,
|
||||
rgb(255 255 255 / 2%) 100%
|
||||
);
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
border: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
&__category-label {
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__category-skeleton {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-radius: 4px;
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
&__section {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
justify-content: space-between;
|
||||
transition: max-height ease 0.5s;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&__category-title {
|
||||
font-size: globals.$small-font-size;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.1s;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-image {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
|
||||
&--locked {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscription-required-button {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: globals.$warning-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import type {
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||
import {
|
||||
|
@ -20,8 +19,8 @@ import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
|||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./sidebar.scss";
|
||||
|
||||
const fakeAchievements: UserAchievement[] = [
|
||||
{
|
||||
|
@ -64,7 +63,6 @@ export function Sidebar() {
|
|||
}>({ isLoading: true, data: null });
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
|
@ -72,10 +70,8 @@ export function Sidebar() {
|
|||
useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -118,7 +114,7 @@ export function Sidebar() {
|
|||
}, [objectId, shop, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className={styles.contentSidebar}>
|
||||
<aside className="content-sidebar">
|
||||
{userDetails === null && (
|
||||
<SidebarSection title={t("achievements")}>
|
||||
<div
|
||||
|
@ -133,21 +129,21 @@ export function Sidebar() {
|
|||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<LockIcon size={36} />
|
||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||
</div>
|
||||
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
|
||||
<ul className="list" style={{ filter: "blur(4px)" }}>
|
||||
{fakeAchievements.map((achievement, index) => (
|
||||
<li key={index}>
|
||||
<div className={styles.listItem}>
|
||||
<div className="list__item">
|
||||
<img
|
||||
style={{ filter: "blur(8px)" }}
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
className={`list__item-image ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
|
@ -164,6 +160,7 @@ export function Sidebar() {
|
|||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{userDetails && achievements && achievements.length > 0 && (
|
||||
<SidebarSection
|
||||
title={t("achievements_count", {
|
||||
|
@ -171,10 +168,10 @@ export function Sidebar() {
|
|||
achievementsCount: achievements.length,
|
||||
})}
|
||||
>
|
||||
<ul className={styles.list}>
|
||||
<ul className="list">
|
||||
{!hasActiveSubscription && (
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
className="subscription-required-button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
<CloudOfflineIcon size={16} />
|
||||
|
@ -190,13 +187,13 @@ export function Sidebar() {
|
|||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
})}
|
||||
className={styles.listItem}
|
||||
className="list__item"
|
||||
title={achievement.description}
|
||||
>
|
||||
<img
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
className={`list__item-image ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
|
@ -227,17 +224,17 @@ export function Sidebar() {
|
|||
|
||||
{stats && (
|
||||
<SidebarSection title={t("stats")}>
|
||||
<div className={styles.statsSection}>
|
||||
<div className={styles.statsCategory}>
|
||||
<p className={styles.statsCategoryTitle}>
|
||||
<div className="stats__section">
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<DownloadIcon size={18} />
|
||||
{t("download_count")}
|
||||
</p>
|
||||
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.statsCategory}>
|
||||
<p className={styles.statsCategoryTitle}>
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<PeopleIcon size={18} />
|
||||
{t("player_count")}
|
||||
</p>
|
||||
|
@ -253,9 +250,9 @@ export function Sidebar() {
|
|||
/>
|
||||
|
||||
<SidebarSection title={t("requirements")}>
|
||||
<div className={styles.requirementButtonContainer}>
|
||||
<div className="requirement__button-container">
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("minimum")}
|
||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||
>
|
||||
|
@ -263,7 +260,7 @@ export function Sidebar() {
|
|||
</Button>
|
||||
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("recommended")}
|
||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||
>
|
||||
|
@ -272,7 +269,7 @@ export function Sidebar() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className={styles.requirementsDetails}
|
||||
className="requirement__details"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
|
|
Loading…
Reference in a new issue