mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 08:43:48 +03:00
Merge pull request #1441 from hydralauncher/feat/migration-to-scss-3-remake
feat: migration to scss
This commit is contained in:
commit
70fcc6e2a1
@ -6,7 +6,6 @@ import {
|
||||
externalizeDepsPlugin,
|
||||
} from "electron-vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
|
||||
@ -55,7 +54,6 @@ export default defineConfig(({ mode }) => {
|
||||
plugins: [
|
||||
svgr(),
|
||||
react(),
|
||||
vanillaExtractPlugin(),
|
||||
sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "hydra-launcher",
|
||||
|
13
package.json
13
package.json
@ -41,9 +41,6 @@
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
"@sentry/react": "^8.47.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
"@vanilla-extract/dynamic": "^2.1.2",
|
||||
"@vanilla-extract/recipes": "^0.5.2",
|
||||
"auto-launch": "^5.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
@ -90,9 +87,8 @@
|
||||
"@swc/core": "^1.4.16",
|
||||
"@types/auto-launch": "^5.0.5",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/folder-hash": "^4.0.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/jsonwebtoken": "^9.0.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/parse-torrent": "^5.8.7",
|
||||
@ -100,14 +96,13 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/sound-play": "^1.1.3",
|
||||
"@types/user-agents": "^1.0.4",
|
||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^31.7.6",
|
||||
"electron": "^31.7.7",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-vite": "^2.0.0",
|
||||
"electron-vite": "^2.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.4.2",
|
||||
|
File diff suppressed because one or more lines are too long
@ -46,9 +46,9 @@ const addGameToLibrary = async (
|
||||
|
||||
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||
|
||||
updateLocalUnlockedAchivements(game!);
|
||||
updateLocalUnlockedAchivements(game);
|
||||
|
||||
createGame(game!).catch(() => {});
|
||||
createGame(game).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,2 @@
|
||||
export { db } from "./level";
|
||||
|
||||
export * from "./sublevels";
|
||||
|
@ -2,5 +2,4 @@ export * from "./downloads";
|
||||
export * from "./games";
|
||||
export * from "./game-shop-cache";
|
||||
export * from "./game-achievements";
|
||||
|
||||
export * from "./keys";
|
||||
|
@ -234,7 +234,9 @@ export class DownloadManager {
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
}
|
||||
|
@ -35,22 +35,20 @@ export const mergeWithRemoteGames = async () => {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
if (steamGame) {
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
objectId: game.objectId,
|
||||
title: steamGame?.name,
|
||||
remoteId: game.id,
|
||||
shop: game.shop,
|
||||
iconUrl,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
isDeleted: false,
|
||||
});
|
||||
}
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
objectId: game.objectId,
|
||||
title: steamGame?.name,
|
||||
remoteId: game.id,
|
||||
shop: game.shop,
|
||||
iconUrl,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
isDeleted: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -21,11 +21,18 @@ export const getSteamAppDetails = async (
|
||||
});
|
||||
|
||||
return axios
|
||||
.get(
|
||||
.get<SteamAppDetailsResponse>(
|
||||
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data[objectId].success) return response.data[objectId].data;
|
||||
if (response.data[objectId].success) {
|
||||
const data = response.data[objectId].data;
|
||||
return {
|
||||
...data,
|
||||
objectId,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -32,10 +32,10 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("pauseGameSeed", shop, objectId),
|
||||
resumeGameSeed: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resumeGameSeed", shop, objectId),
|
||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||
onDownloadProgress: (cb: (value: DownloadProgress | null) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
value: DownloadProgress
|
||||
value: DownloadProgress | null
|
||||
) => cb(value);
|
||||
ipcRenderer.on("on-download-progress", listener);
|
||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||
|
@ -1,134 +0,0 @@
|
||||
import {
|
||||
ComplexStyleRule,
|
||||
createContainer,
|
||||
globalStyle,
|
||||
style,
|
||||
} from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "./theme.css";
|
||||
|
||||
export const appContainer = createContainer();
|
||||
|
||||
globalStyle("*", {
|
||||
boxSizing: "border-box",
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar", {
|
||||
width: "9px",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar-track", {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar-thumb", {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "24px",
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar-thumb:hover", {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.16)",
|
||||
});
|
||||
|
||||
globalStyle("html, body, #root, main", {
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
globalStyle("body", {
|
||||
overflow: "hidden",
|
||||
userSelect: "none",
|
||||
fontFamily: "Noto Sans, sans-serif",
|
||||
fontSize: vars.size.body,
|
||||
color: vars.color.body,
|
||||
margin: "0",
|
||||
});
|
||||
|
||||
globalStyle("button", {
|
||||
padding: "0",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
fontFamily: "inherit",
|
||||
});
|
||||
|
||||
globalStyle("h1, h2, h3, h4, h5, h6, p", {
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
globalStyle("p", {
|
||||
lineHeight: "20px",
|
||||
});
|
||||
|
||||
globalStyle("#root, main", {
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
globalStyle("#root", {
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
globalStyle("main", {
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
globalStyle(
|
||||
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button",
|
||||
{
|
||||
WebkitAppearance: "none",
|
||||
margin: "0",
|
||||
}
|
||||
);
|
||||
|
||||
globalStyle("label", {
|
||||
fontSize: vars.size.body,
|
||||
});
|
||||
|
||||
globalStyle("input[type=number]", {
|
||||
MozAppearance: "textfield",
|
||||
});
|
||||
|
||||
globalStyle("img", {
|
||||
WebkitUserDrag: "none",
|
||||
} as Record<string, string>);
|
||||
|
||||
globalStyle("progress[value]", {
|
||||
WebkitAppearance: "none",
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
containerName: appContainer,
|
||||
containerType: "inline-size",
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
overflowY: "auto",
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 50%, ${vars.color.background} 100%)`,
|
||||
});
|
||||
|
||||
export const titleBar = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "35px",
|
||||
minHeight: "35px",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
alignItems: "center",
|
||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||
WebkitAppRegion: "drag",
|
||||
zIndex: vars.zIndex.titleBar,
|
||||
borderBottom: `1px solid ${vars.color.border}`,
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const cloudText = style({
|
||||
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
||||
backgroundClip: "text",
|
||||
color: "transparent",
|
||||
});
|
136
src/renderer/src/app.scss
Normal file
136
src/renderer/src/app.scss
Normal file
@ -0,0 +1,136 @@
|
||||
@use "./scss/globals.scss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
font-family:
|
||||
Noto Sans,
|
||||
sans-serif;
|
||||
font-size: globals.$body-font-size;
|
||||
color: globals.$body-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#root,
|
||||
main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#root {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
progress[value] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
container-name: globals.$app-container;
|
||||
container-type: inline-size;
|
||||
|
||||
&__content {
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
globals.$dark-background-color 50%,
|
||||
globals.$background-color 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
min-height: 35px;
|
||||
background-color: globals.$dark-background-color;
|
||||
align-items: center;
|
||||
padding: 0 calc(globals.$spacing-unit * 2);
|
||||
-webkit-app-region: drag;
|
||||
z-index: 4;
|
||||
border-bottom: 1px solid globals.$border-color;
|
||||
|
||||
&__cloud-text {
|
||||
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
@ -12,8 +12,6 @@ import {
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./app.css";
|
||||
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
setUserPreferences,
|
||||
@ -29,7 +27,8 @@ import { downloadSourcesWorker } from "./workers";
|
||||
import { downloadSourcesTable } from "./dexie";
|
||||
import { useSubscription } from "./hooks/use-subscription";
|
||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||
import { SPACING_UNIT } from "./theme.css";
|
||||
|
||||
import "./app.scss";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
@ -85,7 +84,7 @@ export function App() {
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onDownloadProgress(
|
||||
(downloadProgress) => {
|
||||
if (downloadProgress.progress === 1) {
|
||||
if (downloadProgress?.progress === 1) {
|
||||
clearDownload();
|
||||
updateLibrary();
|
||||
return;
|
||||
@ -257,34 +256,24 @@ export function App() {
|
||||
return (
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
<div className={styles.titleBar}>
|
||||
<div className="title-bar">
|
||||
<h4>
|
||||
Hydra
|
||||
{hasActiveSubscription && (
|
||||
<span className={styles.cloudText}> Cloud</span>
|
||||
<span className="title-bar__cloud-text"> Cloud</span>
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
||||
right: "16px",
|
||||
maxWidth: "420px",
|
||||
width: "420px",
|
||||
}}
|
||||
>
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
title={toast.title}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
duration={toast.duration}
|
||||
/>
|
||||
</div>
|
||||
<Toast
|
||||
visible={toast.visible}
|
||||
title={toast.title}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={handleToastClose}
|
||||
duration={toast.duration}
|
||||
/>
|
||||
|
||||
<HydraCloudModal
|
||||
visible={isHydraCloudModalVisible}
|
||||
@ -304,10 +293,10 @@ export function App() {
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
<article className={styles.container}>
|
||||
<article className="container">
|
||||
<Header />
|
||||
|
||||
<section ref={contentRef} className={styles.content}>
|
||||
<section ref={contentRef} className="container__content">
|
||||
<Outlet />
|
||||
</section>
|
||||
</article>
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { keyframes } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const backdropFadeIn = keyframes({
|
||||
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
||||
"100%": {
|
||||
backdropFilter: "blur(2px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
},
|
||||
});
|
||||
|
||||
export const backdropFadeOut = keyframes({
|
||||
"0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
|
||||
"100%": {
|
||||
backdropFilter: "blur(0px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
|
||||
export const backdrop = recipe({
|
||||
base: {
|
||||
animationName: backdropFadeIn,
|
||||
animationDuration: "0.4s",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: vars.zIndex.backdrop,
|
||||
top: "0",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
backdropFilter: "blur(2px)",
|
||||
transition: "all ease 0.2s",
|
||||
},
|
||||
variants: {
|
||||
closing: {
|
||||
true: {
|
||||
animationName: backdropFadeOut,
|
||||
backdropFilter: "blur(0px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
},
|
||||
windows: {
|
||||
true: {
|
||||
// SPACING_UNIT * 3 + title bar spacing
|
||||
paddingTop: `${SPACING_UNIT * 3 + 35}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
50
src/renderer/src/components/backdrop/backdrop.scss
Normal file
50
src/renderer/src/components/backdrop/backdrop.scss
Normal file
@ -0,0 +1,50 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.backdrop {
|
||||
animation-name: backdrop-fade-in;
|
||||
animation-duration: 0.4s;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: globals.$backdrop-z-index;
|
||||
top: 0;
|
||||
padding: calc(globals.$spacing-unit * 3);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&--closing {
|
||||
animation-name: backdrop-fade-out;
|
||||
backdrop-filter: blur(0px);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&--windows {
|
||||
padding-top: calc(#{globals.$spacing-unit * 3} + 35);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdrop-fade-in {
|
||||
0% {
|
||||
backdrop-filter: blur(0px);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
100% {
|
||||
backdrop-filter: blur(2px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdrop-fade-out {
|
||||
0% {
|
||||
backdrop-filter: blur(2px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
100% {
|
||||
backdrop-filter: blur(0px);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import * as styles from "./backdrop.css";
|
||||
import "./backdrop.scss";
|
||||
import cn from "classnames";
|
||||
|
||||
export interface BackdropProps {
|
||||
isClosing?: boolean;
|
||||
@ -8,9 +9,9 @@ export interface BackdropProps {
|
||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||
return (
|
||||
<div
|
||||
className={styles.backdrop({
|
||||
closing: isClosing,
|
||||
windows: window.electron.platform === "win32",
|
||||
className={cn("backdrop", {
|
||||
"backdrop--closing": isClosing,
|
||||
"backdrop--windows": window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
@ -7,5 +7,6 @@
|
||||
border: solid 1px globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ export function BottomPanel() {
|
||||
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const isGameDownloading = !!lastPacket;
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||
|
||||
@ -33,9 +31,11 @@ export function BottomPanel() {
|
||||
}, [userDetails?.id]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (isGameDownloading) {
|
||||
const game = library.find((game) => game.id === lastPacket?.gameId)!;
|
||||
const game = lastPacket
|
||||
? library.find((game) => game.id === lastPacket?.gameId)
|
||||
: undefined;
|
||||
|
||||
if (game) {
|
||||
if (lastPacket?.isCheckingFiles)
|
||||
return t("checking_files", {
|
||||
title: game.title,
|
||||
@ -64,7 +64,7 @@ export function BottomPanel() {
|
||||
}
|
||||
|
||||
return t("no_downloads_in_progress");
|
||||
}, [t, isGameDownloading, library, lastPacket, progress, eta, downloadSpeed]);
|
||||
}, [t, library, lastPacket, progress, eta, downloadSpeed]);
|
||||
|
||||
return (
|
||||
<footer className="bottom-panel">
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const checkboxField = style({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const checkbox = recipe({
|
||||
base: {
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
transition: "all ease 0.2s",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
minWidth: "20px",
|
||||
minHeight: "20px",
|
||||
color: vars.color.darkBackground,
|
||||
":hover": {
|
||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
checked: {
|
||||
true: {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const checkboxInput = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
opacity: "0",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const checkboxLabel = style({
|
||||
cursor: "pointer",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.checkbox-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
cursor: pointer;
|
||||
|
||||
&:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: globals.$disabled-opacity;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
border-radius: 4px;
|
||||
background-color: globals.$dark-background-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: all ease 0.2s;
|
||||
border: solid 1px globals.$border-color;
|
||||
|
||||
&:hover:not(:has(input:disabled)) {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
&:has(+ input:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useId } from "react";
|
||||
import * as styles from "./checkbox-field.css";
|
||||
import { CheckIcon } from "@primer/octicons-react";
|
||||
import "./checkbox-field.scss";
|
||||
|
||||
export interface CheckboxFieldProps
|
||||
extends React.DetailedHTMLProps<
|
||||
@ -14,17 +14,19 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className={styles.checkboxField}>
|
||||
<div className={styles.checkbox({ checked: props.checked })}>
|
||||
<div className="checkbox-field">
|
||||
<div
|
||||
className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
className={styles.checkboxInput}
|
||||
className="checkbox-field__input"
|
||||
{...props}
|
||||
/>
|
||||
{props.checked && <CheckIcon />}
|
||||
</div>
|
||||
<label htmlFor={id} className={styles.checkboxLabel}>
|
||||
<label htmlFor={id} className="checkbox-field__label">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const actions = style({
|
||||
display: "flex",
|
||||
alignSelf: "flex-end",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const descriptionText = style({
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.confirmation-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
&__description {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Button } from "../button/button";
|
||||
import { Modal, type ModalProps } from "../modal/modal";
|
||||
|
||||
import * as styles from "./confirmation-modal.css";
|
||||
import "./confirmation-modal.scss";
|
||||
|
||||
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||
confirmButtonLabel: string;
|
||||
@ -31,10 +31,10 @@ export function ConfirmationModal({
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
||||
<div className="confirmation-modal">
|
||||
<p className="confirmation-modal__description">{descriptionText}</p>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<div className="confirmation-modal__actions">
|
||||
<Button theme="outline" onClick={handleCancelClick}>
|
||||
{cancelButtonLabel}
|
||||
</Button>
|
||||
|
@ -1,106 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const card = style({
|
||||
width: "100%",
|
||||
height: "180px",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
transition: "all ease 0.2s",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
":active": {
|
||||
opacity: vars.opacity.active,
|
||||
},
|
||||
});
|
||||
|
||||
export const backdrop = style({
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const cover = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center",
|
||||
position: "absolute",
|
||||
zIndex: "-1",
|
||||
transition: "all ease 0.2s",
|
||||
selectors: {
|
||||
[`${card}:hover &`]: {
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
color: "#DADBE1",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
transition: "all ease 0.2s",
|
||||
transform: "translateY(24px)",
|
||||
selectors: {
|
||||
[`${card}:hover &`]: {
|
||||
transform: "translateY(0px)",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
textAlign: "left",
|
||||
});
|
||||
|
||||
export const downloadOptions = style({
|
||||
display: "flex",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexWrap: "wrap",
|
||||
listStyle: "none",
|
||||
});
|
||||
|
||||
export const specifics = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const specificsItem = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
color: vars.color.muted,
|
||||
fontSize: "12px",
|
||||
alignItems: "flex-end",
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.muted,
|
||||
});
|
||||
|
||||
export const shopIcon = style({
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
minWidth: "20px",
|
||||
});
|
||||
|
||||
export const noDownloadsLabel = style({
|
||||
color: vars.color.body,
|
||||
fontWeight: "bold",
|
||||
});
|
102
src/renderer/src/components/game-card/game-card.scss
Normal file
102
src/renderer/src/components/game-card/game-card.scss
Normal file
@ -0,0 +1,102 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.game-card {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
box-shadow: 0px 0px 15px 0px #000000;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
transition: all ease 0.2s;
|
||||
border: solid 1px globals.$border-color;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&:active {
|
||||
opacity: globals.$active-opacity;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
transition: all ease 0.2s;
|
||||
}
|
||||
|
||||
&__content {
|
||||
color: #dadbe1;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-direction: column;
|
||||
transition: all ease 0.2s;
|
||||
transform: translateY(24px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__download-options {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__specifics {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__specifics-item {
|
||||
gap: globals.$spacing-unit;
|
||||
display: flex;
|
||||
color: globals.$muted-color;
|
||||
font-size: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__shop-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
&__no-download-label {
|
||||
color: globals.$body-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover &__cover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
&:hover &__content {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@ import type { GameStats } from "@types";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
|
||||
import * as styles from "./game-card.css";
|
||||
import "./game-card.scss";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "../badge/badge";
|
||||
import { useCallback, useState } from "react";
|
||||
@ -19,7 +20,7 @@ export interface GameCardProps
|
||||
}
|
||||
|
||||
const shopIcon = {
|
||||
steam: <SteamLogo className={styles.shopIcon} />,
|
||||
steam: <SteamLogo className="game-card__shop-icon" />,
|
||||
};
|
||||
|
||||
export function GameCard({ game, ...props }: GameCardProps) {
|
||||
@ -48,25 +49,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={styles.card}
|
||||
className="game-card"
|
||||
onMouseEnter={handleHover}
|
||||
>
|
||||
<div className={styles.backdrop}>
|
||||
<div className="game-card__backdrop">
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectId)}
|
||||
alt={game.title}
|
||||
className={styles.cover}
|
||||
className="game-card__cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className="game-card__content">
|
||||
<div className="game-card__title-container">
|
||||
{shopIcon[game.shop]}
|
||||
<p className={styles.title}>{game.title}</p>
|
||||
<p className="game-card__title">{game.title}</p>
|
||||
</div>
|
||||
|
||||
{uniqueRepackers.length > 0 ? (
|
||||
<ul className={styles.downloadOptions}>
|
||||
<ul className="game-card__download-options">
|
||||
{uniqueRepackers.map((repacker) => (
|
||||
<li key={repacker}>
|
||||
<Badge>{repacker}</Badge>
|
||||
@ -74,17 +75,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
|
||||
<p className="game-card__no-download-label">{t("no_downloads")}</p>
|
||||
)}
|
||||
<div className={styles.specifics}>
|
||||
<div className={styles.specificsItem}>
|
||||
<div className="game-card__specifics">
|
||||
<div className="game-card__specifics-item">
|
||||
<DownloadIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.specificsItem}>
|
||||
<div className="game-card__specifics-item">
|
||||
<PeopleIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
||||
|
32
src/renderer/src/components/header/auto-update-header.scss
Normal file
32
src/renderer/src/components/header/auto-update-header.scss
Normal file
@ -0,0 +1,32 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.auto-update-sub-header {
|
||||
border-bottom: solid 1px globals.$body-color;
|
||||
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 3);
|
||||
|
||||
&__new-version-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
color: #8e919b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__new-version-icon {
|
||||
color: globals.$success-color;
|
||||
}
|
||||
|
||||
&__new-version-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: globals.$spacing-unit;
|
||||
color: globals.$body-color;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SyncIcon } from "@primer/octicons-react";
|
||||
import { Link } from "../link/link";
|
||||
import * as styles from "./header.css";
|
||||
import "./auto-update-header.scss";
|
||||
import type { AppUpdaterEvent } from "@types";
|
||||
|
||||
export const releasesPageUrl =
|
||||
@ -45,9 +45,15 @@ export function AutoUpdateSubHeader() {
|
||||
|
||||
if (!isAutoInstallAvailable) {
|
||||
return (
|
||||
<header className={styles.subheader}>
|
||||
<Link to={releasesPageUrl} className={styles.newVersionLink}>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
<header className="auto-update-sub-header">
|
||||
<Link
|
||||
to={releasesPageUrl}
|
||||
className="auto-update-sub-header__new-version-link"
|
||||
>
|
||||
<SyncIcon
|
||||
className="auto-update-sub-header__new-version-icon"
|
||||
size={12}
|
||||
/>
|
||||
{t("version_available_download", { version: newVersion })}
|
||||
</Link>
|
||||
</header>
|
||||
@ -56,13 +62,16 @@ export function AutoUpdateSubHeader() {
|
||||
|
||||
if (isReadyToInstall) {
|
||||
return (
|
||||
<header className={styles.subheader}>
|
||||
<header className="auto-update-sub-header">
|
||||
<button
|
||||
type="button"
|
||||
className={styles.newVersionButton}
|
||||
className="auto-update-sub-header__new-version-button"
|
||||
onClick={handleClickInstallUpdate}
|
||||
>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
<SyncIcon
|
||||
className="auto-update-sub-header__new-version-icon"
|
||||
size={12}
|
||||
/>
|
||||
{t("version_available_install", { version: newVersion })}
|
||||
</button>
|
||||
</header>
|
||||
|
@ -1,182 +0,0 @@
|
||||
import type { ComplexStyleRule } from "@vanilla-extract/css";
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: "translateX(20px)", opacity: "0" },
|
||||
"100%": {
|
||||
transform: "translateX(0)",
|
||||
opacity: "1",
|
||||
},
|
||||
});
|
||||
|
||||
export const slideOut = keyframes({
|
||||
"0%": { transform: "translateX(0px)", opacity: "1" },
|
||||
"100%": {
|
||||
transform: "translateX(20px)",
|
||||
opacity: "0",
|
||||
},
|
||||
});
|
||||
|
||||
export const header = recipe({
|
||||
base: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
WebkitAppRegion: "drag",
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
color: vars.color.muted,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
} as ComplexStyleRule,
|
||||
variants: {
|
||||
draggingDisabled: {
|
||||
true: {
|
||||
WebkitAppRegion: "no-drag",
|
||||
} as ComplexStyleRule,
|
||||
},
|
||||
isWindows: {
|
||||
true: {
|
||||
WebkitAppRegion: "no-drag",
|
||||
} as ComplexStyleRule,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const search = recipe({
|
||||
base: {
|
||||
backgroundColor: vars.color.background,
|
||||
display: "inline-flex",
|
||||
transition: "all ease 0.2s",
|
||||
width: "200px",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
height: "40px",
|
||||
WebkitAppRegion: "no-drag",
|
||||
} as ComplexStyleRule,
|
||||
variants: {
|
||||
focused: {
|
||||
true: {
|
||||
width: "250px",
|
||||
borderColor: "#DADBE1",
|
||||
},
|
||||
false: {
|
||||
":hover": {
|
||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const searchInput = style({
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
outline: "none",
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
textOverflow: "ellipsis",
|
||||
":focus": {
|
||||
cursor: "text",
|
||||
},
|
||||
});
|
||||
|
||||
export const actionButton = style({
|
||||
color: "inherit",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.2s",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
":hover": {
|
||||
color: "#DADBE1",
|
||||
},
|
||||
});
|
||||
|
||||
export const section = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const backButton = recipe({
|
||||
base: {
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
WebkitAppRegion: "no-drag",
|
||||
position: "absolute",
|
||||
transition: "transform ease 0.2s",
|
||||
animationDuration: "0.2s",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
} as ComplexStyleRule,
|
||||
variants: {
|
||||
enabled: {
|
||||
true: {
|
||||
animationName: slideIn,
|
||||
},
|
||||
false: {
|
||||
opacity: "0",
|
||||
pointerEvents: "none",
|
||||
animationName: slideOut,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const title = recipe({
|
||||
base: {
|
||||
transition: "all ease 0.2s",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
},
|
||||
variants: {
|
||||
hasBackButton: {
|
||||
true: {
|
||||
transform: "translateX(28px)",
|
||||
width: "calc(100% - 28px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const subheader = style({
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 3}px`,
|
||||
});
|
||||
|
||||
export const newVersionButton = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.body,
|
||||
fontSize: "12px",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
||||
export const newVersionLink = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: "#8e919b",
|
||||
fontSize: "12px",
|
||||
});
|
||||
|
||||
export const newVersionIcon = style({
|
||||
color: vars.color.success,
|
||||
});
|
137
src/renderer/src/components/header/header.scss
Normal file
137
src/renderer/src/components/header/header.scss
Normal file
@ -0,0 +1,137 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
-webkit-app-region: drag;
|
||||
width: 100%;
|
||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||
color: globals.$muted-color;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
|
||||
&--dragging-disabled {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
&--is-windows {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
&__search {
|
||||
background-color: globals.$background-color;
|
||||
display: inline-flex;
|
||||
transition: all ease 0.2s;
|
||||
width: 200px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: solid 1px globals.$border-color;
|
||||
height: 40px;
|
||||
-webkit-app-region: no-drag;
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--focused {
|
||||
width: 250px;
|
||||
border-color: #dadbe1;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
color: #dadbe1;
|
||||
cursor: default;
|
||||
font-family: inherit;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.2s;
|
||||
padding: globals.$spacing-unit;
|
||||
|
||||
&:hover {
|
||||
color: #dadbe1;
|
||||
}
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&--left {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
color: globals.$body-color;
|
||||
cursor: pointer;
|
||||
-webkit-app-region: no-drag;
|
||||
position: absolute;
|
||||
transition: transform ease 0.2s;
|
||||
animation-duration: 0.2s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
animation-name: slide-out;
|
||||
|
||||
&--enabled {
|
||||
animation: slide-in;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
transition: all ease 0.2s;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
|
||||
&--has-back-button {
|
||||
transform: translateX(28px);
|
||||
width: calc(100% - 28px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-out {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@ -5,9 +5,10 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./header.css";
|
||||
import "./header.scss";
|
||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
||||
import { setFilters } from "@renderer/features";
|
||||
import cn from "classnames";
|
||||
|
||||
const pathTitle: Record<string, string> = {
|
||||
"/": "home",
|
||||
@ -75,16 +76,16 @@ export function Header() {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={styles.header({
|
||||
draggingDisabled,
|
||||
isWindows: window.electron.platform === "win32",
|
||||
className={cn("header", {
|
||||
"header--dragging-disabled": draggingDisabled,
|
||||
"header--is-windows": window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
<section className={styles.section} style={{ flex: 1 }}>
|
||||
<section className="header__section header__section--left">
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backButton({
|
||||
enabled: location.key !== "default",
|
||||
className={cn("header__back-button", {
|
||||
"header__back-button--enabled": location.key !== "default",
|
||||
})}
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={location.key === "default"}
|
||||
@ -93,19 +94,23 @@ export function Header() {
|
||||
</button>
|
||||
|
||||
<h3
|
||||
className={styles.title({
|
||||
hasBackButton: location.key !== "default",
|
||||
className={cn("header__title", {
|
||||
"header__title--has-back-button": location.key !== "default",
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.search({ focused: isFocused })}>
|
||||
<section className="header__section">
|
||||
<div
|
||||
className={cn("header__search", {
|
||||
"header__search--focused": isFocused,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.actionButton}
|
||||
className="header__action-button"
|
||||
onClick={focusInput}
|
||||
>
|
||||
<SearchIcon />
|
||||
@ -117,7 +122,7 @@ export function Header() {
|
||||
name="search"
|
||||
placeholder={t("search")}
|
||||
value={searchValue}
|
||||
className={styles.searchInput}
|
||||
className="header__search-input"
|
||||
onChange={(event) => handleSearch(event.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
@ -127,7 +132,7 @@ export function Header() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch(setFilters({ title: "" }))}
|
||||
className={styles.actionButton}
|
||||
className="header__action-button"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const hero = style({
|
||||
width: "100%",
|
||||
height: "280px",
|
||||
minHeight: "280px",
|
||||
maxHeight: "280px",
|
||||
borderRadius: "4px",
|
||||
color: "#DADBE1",
|
||||
overflow: "hidden",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
cursor: "pointer",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const heroMedia = style({
|
||||
objectFit: "cover",
|
||||
objectPosition: "center",
|
||||
position: "absolute",
|
||||
zIndex: "-1",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "all ease 0.2s",
|
||||
imageRendering: "revert",
|
||||
selectors: {
|
||||
[`${hero}:hover &`]: {
|
||||
transform: "scale(1.02)",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const backdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
export const description = style({
|
||||
maxWidth: "700px",
|
||||
color: vars.color.muted,
|
||||
textAlign: "left",
|
||||
lineHeight: "20px",
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-end",
|
||||
});
|
56
src/renderer/src/components/hero/hero.scss
Normal file
56
src/renderer/src/components/hero/hero.scss
Normal file
@ -0,0 +1,56 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.hero {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
max-height: 280px;
|
||||
border-radius: 4px;
|
||||
color: #dadbe1;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 0px 15px 0px #000000;
|
||||
cursor: pointer;
|
||||
border: solid 1px globals.$border-color;
|
||||
z-index: 1;
|
||||
|
||||
&__media {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all ease 0.2s;
|
||||
image-rendering: revert;
|
||||
}
|
||||
&:hover &__media {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__description {
|
||||
max-width: 700px;
|
||||
color: globals.$muted-color;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
margin-top: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as styles from "./hero.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { TrendingGame } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import "./hero.scss";
|
||||
|
||||
export function Hero() {
|
||||
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
||||
@ -29,7 +29,7 @@ export function Hero() {
|
||||
}, [i18n.language]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className={styles.hero} />;
|
||||
return <Skeleton className="hero" />;
|
||||
}
|
||||
|
||||
if (featuredGameDetails?.length) {
|
||||
@ -37,17 +37,17 @@ export function Hero() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(game.uri)}
|
||||
className={styles.hero}
|
||||
className="hero"
|
||||
key={index}
|
||||
>
|
||||
<div className={styles.backdrop}>
|
||||
<div className="hero__backdrop">
|
||||
<img
|
||||
src={game.background}
|
||||
alt={game.description}
|
||||
className={styles.heroMedia}
|
||||
className="hero__media"
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className="hero__content">
|
||||
{game.logo && (
|
||||
<img
|
||||
src={game.logo}
|
||||
@ -56,7 +56,7 @@ export function Hero() {
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
<p className={styles.description}>{game.description}</p>
|
||||
<p className="hero__description">{game.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const link = style({
|
||||
textDecoration: "none",
|
||||
color: "#C0C1C7",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
7
src/renderer/src/components/link/link.scss
Normal file
7
src/renderer/src/components/link/link.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: #c0c1c7;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./link.css";
|
||||
import "./link.scss";
|
||||
|
||||
export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
const openExternal = (event: React.MouseEvent) => {
|
||||
@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
className={cn(styles.link, className)}
|
||||
className={cn("link", className)}
|
||||
onClick={openExternal}
|
||||
{...props}
|
||||
>
|
||||
@ -22,11 +22,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactRouterDomLink
|
||||
className={cn(styles.link, className)}
|
||||
to={to}
|
||||
{...props}
|
||||
>
|
||||
<ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
|
||||
{children}
|
||||
</ReactRouterDomLink>
|
||||
);
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const scaleFadeIn = keyframes({
|
||||
"0%": { opacity: "0", scale: "0.5" },
|
||||
"100%": {
|
||||
opacity: "1",
|
||||
scale: "1",
|
||||
},
|
||||
});
|
||||
|
||||
export const scaleFadeOut = keyframes({
|
||||
"0%": { opacity: "1", scale: "1" },
|
||||
"100%": {
|
||||
opacity: "0",
|
||||
scale: "0.5",
|
||||
},
|
||||
});
|
||||
|
||||
export const modal = recipe({
|
||||
base: {
|
||||
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "4px",
|
||||
minWidth: "400px",
|
||||
maxWidth: "600px",
|
||||
color: vars.color.body,
|
||||
maxHeight: "100%",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
variants: {
|
||||
closing: {
|
||||
true: {
|
||||
animationName: scaleFadeOut,
|
||||
opacity: "0",
|
||||
},
|
||||
},
|
||||
large: {
|
||||
true: {
|
||||
width: "800px",
|
||||
maxWidth: "800px",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const modalContent = style({
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const modalHeader = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const closeModalButton = style({
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.2s",
|
||||
alignSelf: "flex-start",
|
||||
":hover": {
|
||||
opacity: "0.75",
|
||||
},
|
||||
});
|
||||
|
||||
export const closeModalButtonIcon = style({
|
||||
color: vars.color.body,
|
||||
});
|
83
src/renderer/src/components/modal/modal.scss
Normal file
83
src/renderer/src/components/modal/modal.scss
Normal file
@ -0,0 +1,83 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.modal {
|
||||
animation: scale-fade-in 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none
|
||||
running;
|
||||
background-color: globals.$background-color;
|
||||
border-radius: 4px;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
color: globals.$body-color;
|
||||
max-height: 100%;
|
||||
border: solid 1px globals.$border-color;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--closing {
|
||||
animation-name: scale-fade-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: 800px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
cursor: pointer;
|
||||
transition: all ease 0.2s;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button-icon {
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
scale: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
scale: 0.5;
|
||||
}
|
||||
}
|
@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { XIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./modal.css";
|
||||
import "./modal.scss";
|
||||
|
||||
import { Backdrop } from "../backdrop/backdrop";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import cn from "classnames";
|
||||
|
||||
export interface ModalProps {
|
||||
visible: boolean;
|
||||
@ -109,15 +110,18 @@ export function Modal({
|
||||
return createPortal(
|
||||
<Backdrop isClosing={isClosing}>
|
||||
<div
|
||||
className={styles.modal({ closing: isClosing, large })}
|
||||
className={cn("modal", {
|
||||
"modal--closing": isClosing,
|
||||
"modal--large": large,
|
||||
})}
|
||||
role="dialog"
|
||||
aria-labelledby={title}
|
||||
aria-describedby={description}
|
||||
ref={modalContentRef}
|
||||
data-hydra-dialog
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||
<div className="modal__header">
|
||||
<div className="modal__header-title">
|
||||
<h3>{title}</h3>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
@ -125,13 +129,13 @@ export function Modal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseClick}
|
||||
className={styles.closeModalButton}
|
||||
className="modal__close-button"
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<XIcon className={styles.closeModalButtonIcon} size={24} />
|
||||
<XIcon className="modal__close-button-icon" size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>{children}</div>
|
||||
<div className="modal__content">{children}</div>
|
||||
</div>
|
||||
</Backdrop>,
|
||||
document.body
|
||||
|
@ -1,59 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const select = recipe({
|
||||
base: {
|
||||
display: "inline-flex",
|
||||
transition: "all ease 0.2s",
|
||||
width: "fit-content",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
height: "40px",
|
||||
minHeight: "40px",
|
||||
},
|
||||
variants: {
|
||||
focused: {
|
||||
true: {
|
||||
borderColor: "#DADBE1",
|
||||
},
|
||||
false: {
|
||||
":hover": {
|
||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
primary: {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: vars.color.background,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const option = style({
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
borderRight: "4px solid",
|
||||
borderColor: "transparent",
|
||||
borderRadius: "8px",
|
||||
width: "fit-content",
|
||||
height: "100%",
|
||||
outline: "none",
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
fontSize: vars.size.body,
|
||||
textOverflow: "ellipsis",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const label = style({
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
display: "block",
|
||||
color: vars.color.body,
|
||||
});
|
54
src/renderer/src/components/select-field/select-field.scss
Normal file
54
src/renderer/src/components/select-field/select-field.scss
Normal file
@ -0,0 +1,54 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.select-field {
|
||||
display: inline-flex;
|
||||
transition: all ease 0.2s;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid globals.$border-color;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
|
||||
&__container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--focused {
|
||||
border-color: #dadbe1;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background-color: globals.$background-color;
|
||||
}
|
||||
|
||||
&__option {
|
||||
background-color: globals.$dark-background-color;
|
||||
border-right: 4px solid;
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
width: fit-content;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
color: #dadbe1;
|
||||
cursor: default;
|
||||
font-family: inherit;
|
||||
font-size: globals.$body-font-size;
|
||||
text-overflow: ellipsis;
|
||||
padding: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-bottom: globals.$spacing-unit;
|
||||
display: block;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import { useId, useState } from "react";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import * as styles from "./select-field.css";
|
||||
import "./select-field.scss";
|
||||
import cn from "classnames";
|
||||
|
||||
export interface SelectProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||
HTMLSelectElement
|
||||
> {
|
||||
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
|
||||
theme?: "primary" | "dark";
|
||||
label?: string;
|
||||
options?: { key: string; value: string; label: string }[];
|
||||
}
|
||||
@ -23,18 +23,22 @@ export function SelectField({
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="select-field__container">
|
||||
{label && (
|
||||
<label htmlFor={id} className={styles.label}>
|
||||
<label htmlFor={id} className="select-field__label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className={styles.select({ focused: isFocused, theme })}>
|
||||
<div
|
||||
className={cn("select-field", `select-field--${theme}`, {
|
||||
"select-field--focused": isFocused,
|
||||
})}
|
||||
>
|
||||
<select
|
||||
id={id}
|
||||
value={value}
|
||||
className={styles.option}
|
||||
className="select-field__option"
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onChange={onChange}
|
||||
|
@ -1,79 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const profileContainer = style({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const profileButton = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const profileButtonContent = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const profileButtonInformation = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
flex: "1",
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
export const profileButtonTitle = style({
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
export const friendsButton = style({
|
||||
color: vars.color.muted,
|
||||
cursor: "pointer",
|
||||
borderRadius: "50%",
|
||||
width: "40px",
|
||||
minWidth: "40px",
|
||||
minHeight: "40px",
|
||||
height: "40px",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
transition: "all ease 0.3s",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const friendsButtonBadge = style({
|
||||
backgroundColor: vars.color.success,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
right: "-5px",
|
||||
});
|
89
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
89
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
@ -0,0 +1,89 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.sidebar-profile {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.1s;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit globals.$spacing-unit;
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&__button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc(globals.$spacing-unit + globals.$spacing-unit / 2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__button-information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__button-title {
|
||||
font-weight: bold;
|
||||
font-size: globals.$body-font-size;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__friends-button {
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
height: 40px;
|
||||
background-color: globals.$background-color;
|
||||
position: relative;
|
||||
transition: all ease 0.3s;
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&__friends-button-badge {
|
||||
background-color: globals.$success-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
&__game-running-icon {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__button-game-running-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -8,6 +7,7 @@ import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-mo
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { Avatar } from "../avatar/avatar";
|
||||
import { AuthPage } from "@shared";
|
||||
import "./sidebar-profile.scss";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 120_000;
|
||||
|
||||
@ -50,14 +50,14 @@ export function SidebarProfile() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.friendsButton}
|
||||
className="sidebar-profile__friends-button"
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
title={t("friends")}
|
||||
>
|
||||
{friendRequestCount > 0 && (
|
||||
<small className={styles.friendsButtonBadge}>
|
||||
<small className="sidebar-profile__friends-button-badge">
|
||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||
</small>
|
||||
)}
|
||||
@ -73,9 +73,9 @@ export function SidebarProfile() {
|
||||
if (gameRunning.iconUrl) {
|
||||
return (
|
||||
<img
|
||||
className="sidebar-profile__game-running-icon"
|
||||
alt={gameRunning.title}
|
||||
width={24}
|
||||
style={{ borderRadius: 4 }}
|
||||
src={gameRunning.iconUrl}
|
||||
/>
|
||||
);
|
||||
@ -85,34 +85,26 @@ export function SidebarProfile() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.profileContainer}>
|
||||
<div className="sidebar-profile">
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileButton}
|
||||
className="sidebar-profile__button"
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<div className={styles.profileButtonContent}>
|
||||
<div className="sidebar-profile__button-content">
|
||||
<Avatar
|
||||
size={35}
|
||||
src={userDetails?.profileImageUrl}
|
||||
alt={userDetails?.displayName}
|
||||
/>
|
||||
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p className={styles.profileButtonTitle}>
|
||||
<div className="sidebar-profile__button-information">
|
||||
<p className="sidebar-profile__button-title">
|
||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||
</p>
|
||||
|
||||
{userDetails && gameRunning && (
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<div className="sidebar-profile__button-game-running-title">
|
||||
<small>{gameRunning.title}</small>
|
||||
</div>
|
||||
)}
|
||||
|
@ -101,6 +101,12 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__section {
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
|
@ -180,14 +180,7 @@ export function Sidebar() {
|
||||
maxWidth: sidebarWidth,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<div className="sidebar__container">
|
||||
<SidebarProfile />
|
||||
|
||||
<div className="sidebar__content">
|
||||
|
@ -1,89 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const textFieldContainer = style({
|
||||
flex: "1",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const textField = recipe({
|
||||
base: {
|
||||
display: "inline-flex",
|
||||
transition: "all ease 0.2s",
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
height: "40px",
|
||||
minHeight: "40px",
|
||||
},
|
||||
variants: {
|
||||
theme: {
|
||||
primary: {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: vars.color.background,
|
||||
},
|
||||
},
|
||||
hasError: {
|
||||
true: {
|
||||
borderColor: vars.color.danger,
|
||||
},
|
||||
},
|
||||
focused: {
|
||||
true: {
|
||||
borderColor: "#DADBE1",
|
||||
},
|
||||
false: {
|
||||
":hover": {
|
||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const textFieldInput = recipe({
|
||||
base: {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
outline: "none",
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
textOverflow: "ellipsis",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
":focus": {
|
||||
cursor: "text",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
readOnly: {
|
||||
true: {
|
||||
textOverflow: "inherit",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const togglePasswordButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.muted,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const textFieldWrapper = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const errorLabel = style({
|
||||
color: vars.color.danger,
|
||||
});
|
79
src/renderer/src/components/text-field/text-field.scss
Normal file
79
src/renderer/src/components/text-field/text-field.scss
Normal file
@ -0,0 +1,79 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.text-field-container {
|
||||
flex: 1;
|
||||
gap: globals.$spacing-unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__text-field {
|
||||
display: inline-flex;
|
||||
transition: all ease 0.2s;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: solid 1px globals.$border-color;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background-color: globals.$background-color;
|
||||
}
|
||||
|
||||
&--has-error {
|
||||
border-color: globals.$danger-color;
|
||||
}
|
||||
|
||||
&--focused {
|
||||
border-color: #dadbe1;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-field-input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
color: #dadbe1;
|
||||
cursor: default;
|
||||
font-family: inherit;
|
||||
text-overflow: ellipsis;
|
||||
padding: globals.$spacing-unit;
|
||||
|
||||
&:focus {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&--read-only {
|
||||
text-overflow: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-password-button {
|
||||
cursor: pointer;
|
||||
color: globals.$muted-color;
|
||||
padding: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__text-field-wrapper {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__error-label {
|
||||
color: globals.$danger-color;
|
||||
}
|
||||
}
|
@ -1,16 +1,17 @@
|
||||
import React, { useId, useMemo, useState } from "react";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./text-field.css";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./text-field.scss";
|
||||
|
||||
export interface TextFieldProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
||||
theme?: "primary" | "dark";
|
||||
label?: string | React.ReactNode;
|
||||
hint?: string | React.ReactNode;
|
||||
textFieldProps?: React.DetailedHTMLProps<
|
||||
@ -54,7 +55,10 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
}, [props.type, isPasswordVisible]);
|
||||
|
||||
const hintContent = useMemo(() => {
|
||||
if (error) return <small className={styles.errorLabel}>{error}</small>;
|
||||
if (error)
|
||||
return (
|
||||
<small className="text-field-container__error-label">{error}</small>
|
||||
);
|
||||
|
||||
if (hint) return <small>{hint}</small>;
|
||||
return null;
|
||||
@ -73,22 +77,28 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
const hasError = !!error;
|
||||
|
||||
return (
|
||||
<div className={styles.textFieldContainer} {...containerProps}>
|
||||
<div className="text-field-container" {...containerProps}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
||||
<div className={styles.textFieldWrapper}>
|
||||
<div className="text-field-container__text-field-wrapper">
|
||||
<div
|
||||
className={styles.textField({
|
||||
theme,
|
||||
hasError,
|
||||
focused: isFocused,
|
||||
})}
|
||||
className={cn(
|
||||
"text-field-container__text-field",
|
||||
`text-field-container__text-field--${theme}`,
|
||||
{
|
||||
"text-field-container__text-field--has-error": hasError,
|
||||
"text-field-container__text-field--focused": isFocused,
|
||||
}
|
||||
)}
|
||||
{...textFieldProps}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||
className={cn("text-field-container__text-field-input", {
|
||||
"text-field-container__text-field-input--read-only":
|
||||
props.readOnly,
|
||||
})}
|
||||
{...props}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@ -98,7 +108,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
{showPasswordToggleButton && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.togglePasswordButton}
|
||||
className="text-field-container__toggle-password-button"
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
aria-label={t("toggle_password_visibility")}
|
||||
>
|
||||
|
@ -7,8 +7,8 @@
|
||||
background-color: globals.$dark-background-color;
|
||||
border-radius: 4px;
|
||||
border: solid 1px globals.$border-color;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
right: 16px;
|
||||
bottom: 26px + globals.$spacing-unit;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -31,6 +31,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__message-container {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
@ -38,6 +48,7 @@
|
||||
&::-webkit-progress-bar {
|
||||
background-color: globals.$dark-background-color;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: globals.$muted-color;
|
||||
}
|
||||
|
@ -87,13 +87,7 @@ export function Toast({
|
||||
})}
|
||||
>
|
||||
<div className="toast__content">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `8px`,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className="toast__message-container">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
2
src/renderer/src/declaration.d.ts
vendored
2
src/renderer/src/declaration.d.ts
vendored
@ -50,7 +50,7 @@ declare global {
|
||||
pauseGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
resumeGameSeed: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
onDownloadProgress: (
|
||||
cb: (value: DownloadProgress) => void
|
||||
cb: (value: DownloadProgress | null) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onSeedingStatus: (
|
||||
cb: (value: SeedingStatus[]) => void
|
||||
|
@ -18,9 +18,9 @@ export const downloadSlice = createSlice({
|
||||
name: "download",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
||||
setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => {
|
||||
state.lastPacket = action.payload;
|
||||
if (!state.gameId) state.gameId = action.payload.gameId;
|
||||
if (!state.gameId && action.payload) state.gameId = action.payload.gameId;
|
||||
},
|
||||
clearDownload: (state) => {
|
||||
state.lastPacket = null;
|
||||
|
@ -114,7 +114,7 @@ export function useDownload() {
|
||||
pauseSeeding,
|
||||
resumeSeeding,
|
||||
clearDownload: () => dispatch(clearDownload()),
|
||||
setLastPacket: (packet: DownloadProgress) =>
|
||||
setLastPacket: (packet: DownloadProgress | null) =>
|
||||
dispatch(setLastPacket(packet)),
|
||||
};
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const panel = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
backgroundColor: vars.color.background,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const actions = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadDetailsRow = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
color: vars.color.body,
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const downloadsLink = style({
|
||||
color: vars.color.body,
|
||||
textDecoration: "underline",
|
||||
});
|
||||
|
||||
export const progressBar = recipe({
|
||||
base: {
|
||||
position: "absolute",
|
||||
bottom: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "3px",
|
||||
transition: "all ease 0.2s",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: {
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
textAlign: "start",
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
97
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
97
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
@ -0,0 +1,97 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.achievement-panel {
|
||||
width: 100%;
|
||||
padding: globals.$spacing-unit * 2 globals.$spacing-unit * 3;
|
||||
background-color: globals.$background-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__download-details-row {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
text-align: start;
|
||||
color: globals.$body-color;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: globals.$warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
}
|
||||
|
||||
&__grid--with-subscription {
|
||||
grid-template-columns: 3fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&__grid--without-subscription {
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
|
||||
&__points-container {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__content-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
@ -3,8 +3,7 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { UserAchievement } from "@types";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import * as styles from "./achievement-panel.css";
|
||||
import "./achievement-panel.scss";
|
||||
|
||||
export interface AchievementPanelProps {
|
||||
achievements: UserAchievement[];
|
||||
@ -28,17 +27,18 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
<div className="achievement-panel">
|
||||
<div className="achievement-panel__content">
|
||||
{t("earned_points")}{" "}
|
||||
<HydraIcon className="achievement-panel__content-icon" />
|
||||
??? / ???
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className={styles.link}
|
||||
className="achievement-panel__link"
|
||||
>
|
||||
<small style={{ color: vars.color.warning }}>
|
||||
<small className="achievement-panel__link--warning">
|
||||
{t("how_to_earn_achievements_points")}
|
||||
</small>
|
||||
</button>
|
||||
@ -47,9 +47,10 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
<div className="achievement-panel">
|
||||
<div className="achievement-panel__content">
|
||||
{t("earned_points")}{" "}
|
||||
<HydraIcon className="achievement-panel__content-icon" />
|
||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||
</div>
|
||||
</div>
|
||||
|
248
src/renderer/src/pages/achievements/achievements-content.scss
Normal file
248
src/renderer/src/pages/achievements/achievements-content.scss
Normal file
@ -0,0 +1,248 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
$hero-height: 150px;
|
||||
$logo-height: 100px;
|
||||
$logo-max-width: 200px;
|
||||
|
||||
.achievements-content {
|
||||
&__comparison {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: globals.$spacing-unit;
|
||||
|
||||
&__container {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: globals.$spacing-unit;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
|
||||
&__subscription-required-button {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: globals.$spacing-unit / 2;
|
||||
color: globals.$body-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__blured-avatar {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
align-items: center;
|
||||
height: 62px;
|
||||
position: relative;
|
||||
filter: blur(4px);
|
||||
|
||||
h1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__small-avatar {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__subscription-required-button {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
color: globals.$body-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__user-summary {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
align-items: center;
|
||||
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
color: globals.$muted-color;
|
||||
|
||||
&__trophy-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__achievements-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
&__image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__section {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
z-index: 1;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
globals.$background-color 0%,
|
||||
globals.$background-color 100%
|
||||
);
|
||||
|
||||
&__hero {
|
||||
width: 100%;
|
||||
height: $hero-height;
|
||||
min-height: $hero-height;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&__content {
|
||||
padding: globals.$spacing-unit * 2;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__game-logo {
|
||||
width: $logo-max-width;
|
||||
height: $logo-height;
|
||||
object-fit: contain;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__achievements-summary-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: globals.$spacing-unit;
|
||||
}
|
||||
}
|
||||
|
||||
&__table-header {
|
||||
width: 100%;
|
||||
background-color: globals.$background-color;
|
||||
transition: all ease 0.2s;
|
||||
border-bottom: 1px solid globals.$border-color;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
&--stuck {
|
||||
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: grid;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
padding: globals.$spacing-unit * 3;
|
||||
|
||||
&--has-no-active-subscription {
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
|
||||
&--has-active-subscription {
|
||||
grid-template-columns: 3fr 1fr 1fr;
|
||||
}
|
||||
|
||||
&__user-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__other-user-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__profile-avatar {
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: globals.$background-color;
|
||||
position: relative;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
@ -8,18 +8,17 @@ import {
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import type { ComparedAchievements } from "@types";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
import { Link } from "@renderer/components";
|
||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||
import * as styles from "./achievements.css";
|
||||
import { AchievementList } from "./achievement-list";
|
||||
import { AchievementPanel } from "./achievement-panel";
|
||||
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./achievements-content.scss";
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
@ -48,10 +47,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||
) => {
|
||||
return (
|
||||
<div className={styles.profileAvatar}>
|
||||
<div className="achievements-content__profile-avatar">
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
className="achievements-content__profile-avatar"
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
@ -64,91 +63,33 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
|
||||
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<div className="achievements-content__comparison">
|
||||
<div className="achievements-content__comparison__container">
|
||||
<LockIcon size={24} />
|
||||
<h3>
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
className="achievements-content__comparison__container__subscription-required-button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
{t("subscription_needed")}
|
||||
</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
height: "62px",
|
||||
position: "relative",
|
||||
filter: "blur(4px)",
|
||||
}}
|
||||
>
|
||||
<div className="achievements-content__comparison__blured-avatar">
|
||||
{getProfileImage(user)}
|
||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
||||
<h1>{user.displayName}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="achievements-content__user-summary">
|
||||
{getProfileImage(user)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div className="achievements-content__user-summary__container">
|
||||
<h1>{user.displayName}</h1>
|
||||
<div className="achievements-content__user-summary__container__stats">
|
||||
<div className="achievements-content__user-summary__container__stats__trophy-count">
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
||||
@ -164,7 +105,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
<progress
|
||||
max={1}
|
||||
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
||||
className={styles.achievementsProgressBar}
|
||||
className="achievements-content__user-summary__container__stats__progress-bar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -203,7 +144,7 @@ export function AchievementsContent({
|
||||
};
|
||||
|
||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
|
||||
const heroHeight = heroRef.current?.clientHeight ?? 150;
|
||||
|
||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
||||
@ -219,10 +160,10 @@ export function AchievementsContent({
|
||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||
) => {
|
||||
return (
|
||||
<div className={styles.profileAvatarSmall}>
|
||||
<div className="achievements-content__comparison__small-avatar">
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatarSmall}
|
||||
className="achievements-content__comparison__small-avatar"
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
@ -236,10 +177,10 @@ export function AchievementsContent({
|
||||
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className="achievements-content__achievements-list">
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectId)}
|
||||
style={{ display: "none" }}
|
||||
className="achievements-content__achievements-list__image"
|
||||
alt={gameTitle}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
@ -247,38 +188,32 @@ export function AchievementsContent({
|
||||
<section
|
||||
ref={containerRef}
|
||||
onScroll={onScroll}
|
||||
className={styles.container}
|
||||
className="achievements-content__achievements-list__section"
|
||||
>
|
||||
<div
|
||||
className="achievements-content__achievements-list__section__container"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
||||
background: `linear-gradient(0deg, #151515 0%, ${gameColor} 100%)`,
|
||||
}}
|
||||
>
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div className={styles.heroContent}>
|
||||
<div
|
||||
ref={heroRef}
|
||||
className="achievements-content__achievements-list__section__container__hero"
|
||||
>
|
||||
<div className="achievements-content__achievements-list__section__container__hero__content">
|
||||
<Link
|
||||
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectId)}
|
||||
className={styles.gameLogo}
|
||||
className="achievements-content__achievements-list__section__container__hero__content__game-logo"
|
||||
alt={gameTitle}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="achievements-content__achievements-list__section__container__achievements-summary-wrapper">
|
||||
<AchievementSummary
|
||||
user={{
|
||||
...userDetails,
|
||||
@ -298,24 +233,19 @@ export function AchievementsContent({
|
||||
</div>
|
||||
|
||||
{otherUser && (
|
||||
<div className={styles.tableHeader({ stuck: isHeaderStuck })}>
|
||||
<div
|
||||
className={`achievements-content__achievements-list__section__table-header ${isHeaderStuck ? "achievements-content__achievements-list__section__table-header--stuck" : ""}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: hasActiveSubscription
|
||||
? "3fr 1fr 1fr"
|
||||
: "3fr 2fr",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
className={`achievements-content__achievements-list__section__table-header__container ${hasActiveSubscription ? "achievements-content__achievements-list__section__table-header__container--has-active-subscription" : "achievements-content__achievements-list__section__table-header__container--has-no-active-subscription"}`}
|
||||
>
|
||||
<div></div>
|
||||
{hasActiveSubscription && (
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<div className="achievements-content__achievements-list__section__table-header__container__user-avatar">
|
||||
{getProfileImage({ ...userDetails })}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<div className="achievements-content__achievements-list__section__table-header__container__other-user-avatar">
|
||||
{getProfileImage(otherUser)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import * as styles from "./achievements.css";
|
||||
import "./achievements.scss";
|
||||
|
||||
export function AchievementsSkeleton() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
<div className="achievements__container">
|
||||
<div className="achievements__hero">
|
||||
<Skeleton className="achievements__hero-image-skeleton" />
|
||||
</div>
|
||||
<div className={styles.heroPanelSkeleton}></div>
|
||||
<div className="achievements__hero-panel-skeleton"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,197 +0,0 @@
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const HERO_HEIGHT = 150;
|
||||
const LOGO_HEIGHT = 100;
|
||||
const LOGO_MAX_WIDTH = 200;
|
||||
|
||||
export const wrapper = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "all ease 0.3s",
|
||||
});
|
||||
|
||||
export const hero = style({
|
||||
width: "100%",
|
||||
height: `${HERO_HEIGHT}px`,
|
||||
minHeight: `${HERO_HEIGHT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
transition: "all ease 0.2s",
|
||||
});
|
||||
|
||||
export const heroContent = style({
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const gameLogo = style({
|
||||
width: LOGO_MAX_WIDTH,
|
||||
height: LOGO_HEIGHT,
|
||||
objectFit: "contain",
|
||||
transition: "all ease 0.2s",
|
||||
":hover": {
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const tableHeader = recipe({
|
||||
base: {
|
||||
width: "100%",
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
transition: "all ease 0.2s",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
position: "sticky",
|
||||
top: "0",
|
||||
zIndex: "1",
|
||||
},
|
||||
variants: {
|
||||
stuck: {
|
||||
true: {
|
||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const list = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
backgroundColor: vars.color.background,
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
transition: "all ease 0.1s",
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
textDecoration: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export const listItemImage = recipe({
|
||||
base: {
|
||||
width: "54px",
|
||||
height: "54px",
|
||||
borderRadius: "4px",
|
||||
objectFit: "cover",
|
||||
},
|
||||
variants: {
|
||||
unlocked: {
|
||||
false: {
|
||||
filter: "grayscale(100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const achievementsProgressBar = style({
|
||||
width: "100%",
|
||||
height: "8px",
|
||||
transition: "all ease 0.2s",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
|
||||
export const heroLogoBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-end",
|
||||
});
|
||||
|
||||
export const heroImageSkeleton = style({
|
||||
height: "150px",
|
||||
});
|
||||
|
||||
export const heroPanelSkeleton = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const listItemSkeleton = style({
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const profileAvatar = style({
|
||||
height: "54px",
|
||||
width: "54px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const profileAvatarSmall = style({
|
||||
height: "32px",
|
||||
width: "32px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
position: "relative",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const subscriptionRequiredButton = style({
|
||||
textDecoration: "none",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
@ -66,9 +66,9 @@ $logo-max-width: 200px;
|
||||
|
||||
&__table-header {
|
||||
width: 100%;
|
||||
background-color: var(--color-dark-background);
|
||||
background-color: globals.$dark-background-color;
|
||||
transition: all ease 0.2s;
|
||||
border-bottom: solid 1px var(--color-border);
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
@ -86,13 +86,13 @@ $logo-max-width: 200px;
|
||||
gap: globals.$spacing-unit * 2;
|
||||
padding: globals.$spacing-unit * 2;
|
||||
width: 100%;
|
||||
background-color: var(--color-background);
|
||||
background-color: globals.$background-color;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
transition: all ease 0.1s;
|
||||
color: var(--color-muted);
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
@ -102,7 +102,7 @@ $logo-max-width: 200px;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
background-color: globals.$border-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ $logo-max-width: 200px;
|
||||
|
||||
&-hidden-icon {
|
||||
display: flex;
|
||||
color: var(--color-warning);
|
||||
color: globals.$warning-color;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
@ -164,7 +164,7 @@ $logo-max-width: 200px;
|
||||
|
||||
&--locked {
|
||||
cursor: pointer;
|
||||
color: var(--color-warning);
|
||||
color: globals.$warning-color;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@ -219,12 +219,12 @@ $logo-max-width: 200px;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
background-color: globals.$border-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: var(--color-muted);
|
||||
background-color: globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
@ -236,7 +236,7 @@ $logo-max-width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-background);
|
||||
background-color: globals.$background-color;
|
||||
position: relative;
|
||||
object-fit: cover;
|
||||
|
||||
@ -252,7 +252,7 @@ $logo-max-width: 200px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: math.div(globals.$spacing-unit, 2);
|
||||
color: var(--color-body);
|
||||
color: globals.$muted-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
@ -3,7 +3,6 @@ import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
||||
import type { ComparedAchievements, GameShop } from "@types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import {
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
@ -75,10 +74,7 @@ export default function Achievements() {
|
||||
(otherUserId && comparedAchievements === null);
|
||||
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||
{showSkeleton ? (
|
||||
<AchievementsSkeleton />
|
||||
) : (
|
||||
|
@ -1,12 +1,11 @@
|
||||
import type { ComparedAchievements } from "@types";
|
||||
import * as styles from "./achievements.css";
|
||||
import "./achievements.scss";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
EyeClosedIcon,
|
||||
LockIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface ComparedAchievementListProps {
|
||||
@ -20,39 +19,26 @@ export function ComparedAchievementList({
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
<ul className="achievements__list">
|
||||
{achievements.achievements.map((achievement, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={styles.listItem}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: achievement.ownerStat
|
||||
? "3fr 1fr 1fr"
|
||||
: "3fr 2fr",
|
||||
}}
|
||||
className={`achievements__item achievements__item-compared ${
|
||||
!achievement.ownerStat && "achievements__item-compared--no-owner"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<div className="achievements__item-main">
|
||||
<img
|
||||
className={styles.listItemImage({
|
||||
unlocked: true,
|
||||
})}
|
||||
className="achievements__item-image"
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div>
|
||||
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<div className="achievements__item-content">
|
||||
<h4 className="achievements__item-title">
|
||||
{achievement.hidden && (
|
||||
<span
|
||||
style={{ display: "flex" }}
|
||||
className="achievements__item-hidden-icon"
|
||||
title={t("hidden_achievement_tooltip")}
|
||||
>
|
||||
<EyeClosedIcon size={12} />
|
||||
@ -67,25 +53,13 @@ export function ComparedAchievementList({
|
||||
{achievement.ownerStat ? (
|
||||
achievement.ownerStat.unlocked ? (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
className="achievements__item-status achievements__item-status--unlocked"
|
||||
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div className="achievements__item-status">
|
||||
<LockIcon />
|
||||
</div>
|
||||
)
|
||||
@ -93,25 +67,13 @@ export function ComparedAchievementList({
|
||||
|
||||
{achievement.targetStat.unlocked ? (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
className="achievements__item-status achievements__item-status--unlocked"
|
||||
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div className="achievements__item-status">
|
||||
<LockIcon />
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievement-panel.css";
|
||||
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { ComparedAchievements } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import "./achievement-panel.scss";
|
||||
|
||||
export interface ComparedAchievementPanelProps {
|
||||
achievements: ComparedAchievements;
|
||||
@ -18,25 +16,25 @@ export function ComparedAchievementPanel({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.panel}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
className={`achievement-panel achievement-panel__grid ${
|
||||
hasActiveSubscription
|
||||
? "achievement-panel__grid--with-subscription"
|
||||
: "achievement-panel__grid--without-subscription"
|
||||
}`}
|
||||
>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{t("available_points")} <HydraIcon width={20} height={20} />{" "}
|
||||
<div className="achievement-panel__points-container">
|
||||
{t("available_points")}{" "}
|
||||
<HydraIcon className="achievement-panel__content-icon" />{" "}
|
||||
{achievements.achievementsPointsTotal}
|
||||
</div>
|
||||
{hasActiveSubscription && (
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
<div className="achievement-panel__content">
|
||||
<HydraIcon className="achievement-panel__content-icon" />
|
||||
{achievements.owner.achievementsPointsEarnedSum ?? 0}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
<div className="achievement-panel__content">
|
||||
<HydraIcon className="achievement-panel__content-icon" />
|
||||
{achievements.target.achievementsPointsEarnedSum}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,4 +19,62 @@
|
||||
border: 1px solid globals.$border-color;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__filters-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__filters-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__games-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__pagination-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__result-count {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__filters-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__skeleton {
|
||||
height: 105px;
|
||||
border-radius: 4px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import "./catalogue.scss";
|
||||
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { downloadSourcesTable } from "@renderer/dexie";
|
||||
import { FilterSection } from "./filter-section";
|
||||
import { setFilters, setPage } from "@renderer/features";
|
||||
@ -230,25 +229,9 @@ export default function Catalogue() {
|
||||
|
||||
return (
|
||||
<div className="catalogue" ref={cataloguePageRef}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<ul
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
flexWrap: "wrap",
|
||||
listStyle: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<div className="catalogue__header">
|
||||
<div className="catalogue__filters-wrapper">
|
||||
<ul className="catalogue__filters-list">
|
||||
{groupedFilters.map((filter) => (
|
||||
<li key={`${filter.key}-${filter.value}`}>
|
||||
<FilterItem
|
||||
@ -270,50 +253,20 @@ export default function Catalogue() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div className="catalogue__content">
|
||||
<div className="catalogue__games-container">
|
||||
{isLoading ? (
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.darkBackground}
|
||||
highlightColor={vars.color.background}
|
||||
>
|
||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
style={{
|
||||
height: 105,
|
||||
borderRadius: 4,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
}}
|
||||
/>
|
||||
<Skeleton key={i} className="catalogue__skeleton" />
|
||||
))}
|
||||
</SkeletonTheme>
|
||||
) : (
|
||||
results.map((game) => <GameItem key={game.id} game={game} />)
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12 }}>
|
||||
<div className="catalogue__pagination-container">
|
||||
<span className="catalogue__result-count">
|
||||
{t("result_count", {
|
||||
resultCount: formatNumber(itemsCount),
|
||||
})}
|
||||
@ -333,7 +286,7 @@ export default function Catalogue() {
|
||||
</div>
|
||||
|
||||
<div className="catalogue__filters-container">
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<div className="catalogue__filters-sections">
|
||||
{filterSections.map((section) => (
|
||||
<FilterSection
|
||||
key={section.key}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { XIcon } from "@primer/octicons-react";
|
||||
import "./filter.scss";
|
||||
|
||||
interface FilterItemProps {
|
||||
filter: string;
|
||||
@ -9,39 +9,13 @@ interface FilterItemProps {
|
||||
|
||||
export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: vars.color.body,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
padding: "6px 12px",
|
||||
borderRadius: 4,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
backgroundColor: orbColor,
|
||||
borderRadius: "50%",
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
<div className="filter-item">
|
||||
<div className="filter-item__orb" style={{ backgroundColor: orbColor }} />
|
||||
{filter}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
style={{
|
||||
color: vars.color.body,
|
||||
marginLeft: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="filter-item__remove-button"
|
||||
>
|
||||
<XIcon size={13} />
|
||||
</button>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { CheckboxField, TextField } from "@renderer/components";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import "./filter.scss";
|
||||
import List from "rc-virtual-list";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface FilterSectionProps {
|
||||
@ -54,36 +53,18 @@ export function FilterSection({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<div className="filter-section__header">
|
||||
<div
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
className="filter-section__orb"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<h3 className="filter-section__title">{title}</h3>
|
||||
</div>
|
||||
|
||||
{selectedItemsCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 12,
|
||||
display: "block",
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
className="filter-section__clear-button"
|
||||
onClick={onClear}
|
||||
>
|
||||
{t("clear_filters", {
|
||||
@ -91,7 +72,7 @@ export function FilterSection({
|
||||
})}
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ fontSize: 12, marginBottom: 12, display: "block" }}>
|
||||
<span className="filter-section__count">
|
||||
{t("filter_count", {
|
||||
filterCount: formatNumber(items.length),
|
||||
})}
|
||||
@ -102,7 +83,7 @@ export function FilterSection({
|
||||
placeholder={t("search")}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
value={search}
|
||||
containerProps={{ style: { marginBottom: 16 } }}
|
||||
containerProps={{ className: "filter-section__search" }}
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
@ -122,7 +103,7 @@ export function FilterSection({
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<div key={item.value} style={{ height: 28, maxHeight: 28 }}>
|
||||
<div key={item.value} className="filter-section__item">
|
||||
<CheckboxField
|
||||
label={item.label}
|
||||
checked={item.checked}
|
||||
|
71
src/renderer/src/pages/catalogue/filter.scss
Normal file
71
src/renderer/src/pages/catalogue/filter.scss
Normal file
@ -0,0 +1,71 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: globals.$body-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: solid 1px globals.$border-color;
|
||||
font-size: 12px;
|
||||
|
||||
&__orb {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__remove-button {
|
||||
color: globals.$body-color;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
&__header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__orb {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__clear-button {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
color: globals.$body-color;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
height: 28px;
|
||||
max-height: 28px;
|
||||
}
|
||||
}
|
21
src/renderer/src/pages/catalogue/pagination.scss
Normal file
21
src/renderer/src/pages/catalogue/pagination.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.pagination {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
&__button {
|
||||
width: 40px;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
&__ellipsis {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Button } from "@renderer/components/button/button";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
|
||||
import { useFormat } from "@renderer/hooks/use-format";
|
||||
import "./pagination.scss";
|
||||
|
||||
interface PaginationProps {
|
||||
page: number;
|
||||
@ -31,17 +32,12 @@ export function Pagination({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div className="pagination">
|
||||
{/* Previous Button */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
|
||||
className="pagination__button"
|
||||
disabled={page === 1}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
@ -53,22 +49,15 @@ export function Pagination({
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(1)}
|
||||
style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
|
||||
className="pagination__button"
|
||||
disabled={page === 1}
|
||||
>
|
||||
{1}
|
||||
</Button>
|
||||
|
||||
{/* ellipsis */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16 }}>...</span>
|
||||
<div className="pagination__ellipsis">
|
||||
<span className="pagination__ellipsis-text">...</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -81,7 +70,7 @@ export function Pagination({
|
||||
<Button
|
||||
theme={page === pageNumber ? "primary" : "outline"}
|
||||
key={pageNumber}
|
||||
style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
|
||||
className="pagination__button"
|
||||
onClick={() => onPageChange(pageNumber)}
|
||||
>
|
||||
{formatNumber(pageNumber)}
|
||||
@ -91,22 +80,15 @@ export function Pagination({
|
||||
{page < totalPages - 1 && (
|
||||
<>
|
||||
{/* ellipsis */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16 }}>...</span>
|
||||
<div className="pagination__ellipsis">
|
||||
<span className="pagination__ellipsis-text">...</span>
|
||||
</div>
|
||||
|
||||
{/* last page */}
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
|
||||
className="pagination__button"
|
||||
disabled={page === totalPages}
|
||||
>
|
||||
{formatNumber(totalPages)}
|
||||
@ -118,7 +100,7 @@ export function Pagination({
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
|
||||
className="pagination__button"
|
||||
disabled={page === totalPages}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const deleteActionsButtonsCtn = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "end",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
11
src/renderer/src/pages/downloads/delete-game-modal.scss
Normal file
11
src/renderer/src/pages/downloads/delete-game-modal.scss
Normal file
@ -0,0 +1,11 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.delete-game-modal {
|
||||
&__actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
|
||||
import * as styles from "./delete-game-modal.css";
|
||||
import "./delete-game-modal.scss";
|
||||
|
||||
interface DeleteGameModalProps {
|
||||
visible: boolean;
|
||||
@ -29,7 +27,7 @@ export function DeleteGameModal({
|
||||
description={t("delete_modal_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.deleteActionsButtonsCtn}>
|
||||
<div className="delete-game-modal__actions">
|
||||
<Button onClick={handleDeleteGame} theme="outline">
|
||||
{t("delete")}
|
||||
</Button>
|
||||
|
@ -31,8 +31,6 @@ import {
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
export interface DownloadGroupProps {
|
||||
library: LibraryGame[];
|
||||
title: string;
|
||||
@ -287,24 +285,14 @@ export function DownloadGroup({
|
||||
|
||||
<div className="download-group__cover-content">
|
||||
{game.download?.downloader === Downloader.TorBox ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
background: "#11141b",
|
||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT}px`,
|
||||
borderRadius: "4px",
|
||||
gap: 4,
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
}}
|
||||
>
|
||||
<Badge>
|
||||
<img
|
||||
src={torBoxLogo}
|
||||
alt="TorBox"
|
||||
style={{ width: 13 }}
|
||||
/>
|
||||
<span style={{ fontSize: 10 }}>TorBox</span>
|
||||
</div>
|
||||
<span>TorBox</span>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge>
|
||||
{DOWNLOADER_NAME[game.download!.downloader]}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const downloadsContainer = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const downloadGroups = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const arrowIcon = style({
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const noDownloads = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
37
src/renderer/src/pages/downloads/downloads.scss
Normal file
37
src/renderer/src/pages/downloads/downloads.scss
Normal file
@ -0,0 +1,37 @@
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.downloads {
|
||||
&__container {
|
||||
display: flex;
|
||||
padding: calc(globals.$spacing-unit * 3);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__groups {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 3);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__arrow-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__no-downloads {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import "./downloads.scss";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||
@ -125,8 +125,8 @@ export default function Downloads() {
|
||||
/>
|
||||
|
||||
{hasItemsInLibrary ? (
|
||||
<section className={styles.downloadsContainer}>
|
||||
<div className={styles.downloadGroups}>
|
||||
<section className="downloads__container">
|
||||
<div className="downloads__groups">
|
||||
{downloadGroups.map((group) => (
|
||||
<DownloadGroup
|
||||
key={group.title}
|
||||
@ -140,8 +140,8 @@ export default function Downloads() {
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<div className={styles.noDownloads}>
|
||||
<div className={styles.arrowIcon}>
|
||||
<div className="downloads__no-downloads">
|
||||
<div className="downloads__arrow-icon">
|
||||
<ArrowDownIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_downloads_title")}</h2>
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const mappingMethods = style({
|
||||
display: "grid",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
});
|
||||
|
||||
export const fileList = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const fileItem = style({
|
||||
flex: 1,
|
||||
color: vars.color.muted,
|
||||
textDecoration: "underline",
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
});
|
@ -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)}
|
||||
|
@ -1,65 +0,0 @@
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const rotate = keyframes({
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": {
|
||||
transform: "rotate(360deg)",
|
||||
},
|
||||
});
|
||||
|
||||
export const artifacts = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
});
|
||||
|
||||
export const artifactButton = style({
|
||||
display: "flex",
|
||||
textAlign: "left",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.body,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
justifyContent: "space-between",
|
||||
});
|
||||
|
||||
export const syncIcon = style({
|
||||
animationName: rotate,
|
||||
animationDuration: "1s",
|
||||
animationIterationCount: "infinite",
|
||||
animationTimingFunction: "linear",
|
||||
});
|
||||
|
||||
export const progress = style({
|
||||
width: "100%",
|
||||
height: "5px",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
});
|
||||
|
||||
export const manageFilesButton = style({
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
alignSelf: "flex-start",
|
||||
fontSize: 14,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
color: vars.color.body,
|
||||
":disabled": {
|
||||
cursor: "not-allowed",
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
});
|
@ -0,0 +1,119 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
&__backup-state-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
@ -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"> {}
|
||||
@ -94,8 +93,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
const backupStateLabel = useMemo(() => {
|
||||
if (uploadingBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<span className="cloud-sync-modal__backup-state-label">
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
{t("uploading_backup")}
|
||||
</span>
|
||||
);
|
||||
@ -103,8 +102,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
if (restoringBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<span className="cloud-sync-modal__backup-state-label">
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
{t("restoring_backup", {
|
||||
progress: formatDownloadProgress(
|
||||
backupDownloadProgress?.progress ?? 0
|
||||
@ -116,8 +115,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
if (loadingPreview) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
<span className="cloud-sync-modal__backup-state-label">
|
||||
<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,28 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
artifacts.length >= backupsPerGameLimit
|
||||
}
|
||||
>
|
||||
<UploadIcon />
|
||||
{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,
|
||||
}}
|
||||
>
|
||||
<h2>{t("backups")}</h2>
|
||||
<small>
|
||||
{artifacts.length} / {backupsPerGameLimit}
|
||||
</small>
|
||||
</div>
|
||||
<div className="cloud-sync-modal__backups-header">
|
||||
<h2>{t("backups")}</h2>
|
||||
<small>
|
||||
{artifacts.length} / {backupsPerGameLimit}
|
||||
</small>
|
||||
</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 +210,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}
|
||||
>
|
||||
<HistoryIcon />
|
||||
{restoringBackup ? (
|
||||
<SyncIcon className="cloud-sync-modal__sync-icon" />
|
||||
) : (
|
||||
<HistoryIcon />
|
||||
)}
|
||||
{t("install_backup")}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const descriptionHeader = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
});
|
||||
|
||||
export const descriptionHeaderInfo = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
});
|
@ -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,
|
||||
|
@ -1,131 +0,0 @@
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const gallerySliderContainer = style({
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const gallerySliderMedia = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "block",
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
transition: "translate 0.3s ease-in-out",
|
||||
borderRadius: "4px",
|
||||
alignSelf: "center",
|
||||
});
|
||||
|
||||
export const gallerySliderAnimationContainer = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"@media": {
|
||||
"(min-width: 1280px)": {
|
||||
width: "60%",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const gallerySliderPreview = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT}px 0`,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
"@media": {
|
||||
"(min-width: 1280px)": {
|
||||
width: "60%",
|
||||
},
|
||||
},
|
||||
"::-webkit-scrollbar-thumb": {
|
||||
width: "20%",
|
||||
},
|
||||
"::-webkit-scrollbar": {
|
||||
height: "10px",
|
||||
},
|
||||
});
|
||||
|
||||
export const mediaPreviewButton = recipe({
|
||||
base: {
|
||||
cursor: "pointer",
|
||||
width: "20%",
|
||||
display: "block",
|
||||
flexShrink: "0",
|
||||
flexGrow: "0",
|
||||
opacity: "0.3",
|
||||
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
":hover": {
|
||||
opacity: "0.8",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
opacity: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const mediaPreview = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
export const gallerySliderButton = recipe({
|
||||
base: {
|
||||
position: "absolute",
|
||||
alignSelf: "center",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
borderRadius: "50%",
|
||||
color: vars.color.muted,
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
},
|
||||
":active": {
|
||||
transform: "scale(0.95)",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
direction: {
|
||||
left: {
|
||||
left: "0",
|
||||
marginLeft: `${SPACING_UNIT}px`,
|
||||
transform: `translateX(${-(48 + SPACING_UNIT)}px)`,
|
||||
},
|
||||
right: {
|
||||
right: "0",
|
||||
marginRight: `${SPACING_UNIT}px`,
|
||||
transform: `translateX(${48 + SPACING_UNIT}px)`,
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
true: {
|
||||
transform: "translateX(0)",
|
||||
opacity: "1",
|
||||
},
|
||||
false: {
|
||||
opacity: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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,10 +132,11 @@ 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,
|
||||
@ -141,35 +145,26 @@ export function GameDetailsContent() {
|
||||
/>
|
||||
|
||||
<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 +175,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 +184,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} />
|
||||
))}
|
||||
|
@ -1,234 +0,0 @@
|
||||
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const HERO_HEIGHT = 300;
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)`, opacity: "0px" },
|
||||
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||
});
|
||||
|
||||
export const wrapper = recipe({
|
||||
base: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "all ease 0.3s",
|
||||
},
|
||||
variants: {
|
||||
blurredContent: {
|
||||
true: {
|
||||
filter: "blur(20px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const hero = style({
|
||||
width: "100%",
|
||||
height: `${HERO_HEIGHT}px`,
|
||||
minHeight: `${HERO_HEIGHT}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
transition: "all ease 0.2s",
|
||||
"@media": {
|
||||
"(min-width: 1250px)": {
|
||||
height: "350px",
|
||||
minHeight: "350px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const heroContent = style({
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
});
|
||||
|
||||
export const heroLogoBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)",
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
});
|
||||
|
||||
export const heroImage = style({
|
||||
width: "100%",
|
||||
height: `${HERO_HEIGHT}px`,
|
||||
minHeight: `${HERO_HEIGHT}px`,
|
||||
objectFit: "cover",
|
||||
objectPosition: "top",
|
||||
transition: "all ease 0.2s",
|
||||
position: "absolute",
|
||||
zIndex: "0",
|
||||
"@media": {
|
||||
"(min-width: 1250px)": {
|
||||
objectPosition: "center",
|
||||
height: "350px",
|
||||
minHeight: "350px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const gameLogo = style({
|
||||
width: 300,
|
||||
alignSelf: "flex-end",
|
||||
});
|
||||
|
||||
export const heroImageSkeleton = style({
|
||||
height: "300px",
|
||||
"@media": {
|
||||
"(min-width: 1250px)": {
|
||||
height: "350px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const descriptionContainer = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flex: "1",
|
||||
background: `linear-gradient(0deg, ${vars.color.background} 50%, ${vars.color.darkBackground} 100%)`,
|
||||
});
|
||||
|
||||
export const descriptionContent = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
export const description = style({
|
||||
userSelect: "text",
|
||||
lineHeight: "22px",
|
||||
fontSize: "16px",
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
"@media": {
|
||||
"(min-width: 1280px)": {
|
||||
width: "60%",
|
||||
},
|
||||
},
|
||||
width: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
});
|
||||
|
||||
export const descriptionSkeleton = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
|
||||
width: "100%",
|
||||
"@media": {
|
||||
"(min-width: 1280px)": {
|
||||
width: "60%",
|
||||
lineHeight: "22px",
|
||||
},
|
||||
},
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
});
|
||||
|
||||
export const randomizerButton = style({
|
||||
animationName: slideIn,
|
||||
animationDuration: "0.2s",
|
||||
position: "fixed",
|
||||
bottom: `${SPACING_UNIT * 3}px`,
|
||||
/* Scroll bar + spacing */
|
||||
right: `${9 + SPACING_UNIT * 2}px`,
|
||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
||||
border: `solid 2px ${vars.color.border}`,
|
||||
zIndex: "1",
|
||||
backgroundColor: vars.color.background,
|
||||
":hover": {
|
||||
backgroundColor: vars.color.background,
|
||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 15px 5px",
|
||||
opacity: "1",
|
||||
},
|
||||
":active": {
|
||||
transform: "scale(0.98)",
|
||||
},
|
||||
":disabled": {
|
||||
boxShadow: "none",
|
||||
transform: "none",
|
||||
opacity: "0.8",
|
||||
backgroundColor: vars.color.background,
|
||||
},
|
||||
});
|
||||
|
||||
export const heroPanelSkeleton = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
globalStyle(".bb_tag", {
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
globalStyle(`${description} img`, {
|
||||
borderRadius: "5px",
|
||||
marginTop: `${SPACING_UNIT}px`,
|
||||
marginBottom: `${SPACING_UNIT * 3}px`,
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
globalStyle(`${description} a`, {
|
||||
color: vars.color.body,
|
||||
});
|
||||
|
||||
export const cloudSyncButton = style({
|
||||
padding: `${SPACING_UNIT * 1.5}px ${SPACING_UNIT * 2}px`,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
backdropFilter: "blur(20px)",
|
||||
borderRadius: "8px",
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
minHeight: "40px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.muted,
|
||||
fontSize: "14px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.8)",
|
||||
animation: `${slideIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
||||
animationDuration: "0.3s",
|
||||
":active": {
|
||||
opacity: "0.9",
|
||||
},
|
||||
":disabled": {
|
||||
opacity: vars.opacity.disabled,
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
":hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
});
|
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,9 +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";
|
||||
|
||||
import { GameDetailsContent } from "./game-details-content";
|
||||
import {
|
||||
@ -27,6 +24,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);
|
||||
@ -149,10 +147,7 @@ export default function GameDetails() {
|
||||
)}
|
||||
</CloudSyncContextConsumer>
|
||||
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
||||
|
||||
<RepacksModal
|
||||
@ -186,23 +181,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")}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const heroPanelAction = style({
|
||||
border: `solid 1px ${vars.color.muted}`,
|
||||
});
|
||||
|
||||
export const actions = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const separator = style({
|
||||
width: "1px",
|
||||
backgroundColor: vars.color.muted,
|
||||
opacity: "0.2",
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user