diff --git a/src/renderer/pages/game-details/game-details-skeleton.tsx b/src/renderer/pages/game-details/game-details-skeleton.tsx new file mode 100644 index 000000000..73caef4fe --- /dev/null +++ b/src/renderer/pages/game-details/game-details-skeleton.tsx @@ -0,0 +1,98 @@ +import Skeleton from "react-loading-skeleton"; + +import { Button } from "@renderer/components"; +import * as styles from "./game-details.css"; +import { useTranslation } from "react-i18next"; +import { ShareAndroidIcon } from "@primer/octicons-react"; + +export function GameDetailsSkeleton() { + const { t } = useTranslation("game_details"); + + return ( +
+
+ +
+
+
+ + +
+ +
+ + +
+
+
+
+
+
+ + +
+ +
+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} + + {Array.from({ length: 2 }).map((_, index) => ( + + ))} + + +
+
+
+
+

HowLongToBeat

+
+
    + {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+

{t("requirements")}

+
+
+ + +
+
+ {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/renderer/pages/game-details/game-details.css.ts b/src/renderer/pages/game-details/game-details.css.ts index aa2ca1db5..6145b9aed 100644 --- a/src/renderer/pages/game-details/game-details.css.ts +++ b/src/renderer/pages/game-details/game-details.css.ts @@ -34,6 +34,12 @@ export const heroBackdrop = style({ justifyContent: "space-between", }); +export const heroFooterButtonsSkeleton = style({ + display: "flex", + flexDirection: "row", + gap: `${SPACING_UNIT}px`, +}); + export const heroImage = style({ width: "100%", height: "100%", @@ -47,6 +53,15 @@ export const heroImage = style({ }, }); +export const heroImageSkeleton = style({ + height: "300px", + "@media": { + "(min-width: 1250px)": { + height: "350px", + }, + }, +}); + export const container = style({ width: "100%", height: "100%", @@ -114,6 +129,14 @@ export const requirementsDetails = style({ fontSize: "16px", }); +export const requirementsDetailsSkeleton = style({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: `${SPACING_UNIT * 2}px`, + fontSize: "16px", +}); + export const description = style({ userSelect: "text", lineHeight: "22px", @@ -130,6 +153,22 @@ export const description = style({ 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 descriptionHeader = style({ width: "100%", padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, diff --git a/src/renderer/pages/game-details/game-details.tsx b/src/renderer/pages/game-details/game-details.tsx index 3ebab32f2..434cf2076 100644 --- a/src/renderer/pages/game-details/game-details.tsx +++ b/src/renderer/pages/game-details/game-details.tsx @@ -1,7 +1,7 @@ -import { useCallback, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; import Color from "color"; import { average } from "color.js"; +import { useCallback, useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; import type { Game, @@ -13,21 +13,25 @@ import type { import { AsyncImage, Button } from "@renderer/components"; import { setHeaderTitle } from "@renderer/features"; -import { useAppDispatch, useDownload } from "@renderer/hooks"; import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers"; +import { useAppDispatch, useDownload } from "@renderer/hooks"; +import { ShareAndroidIcon } from "@primer/octicons-react"; +import { vars } from "@renderer/theme.css"; +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 { RepacksModal } from "./repacks-modal"; import { HeroPanel } from "./hero-panel"; -import { useTranslation } from "react-i18next"; -import { ShareAndroidIcon } from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; +import { RepacksModal } from "./repacks-modal"; const OPEN_HYDRA_URL = "https://open.hydralauncher.site"; export function GameDetails() { const { objectID, shop } = useParams(); + const [isLoading, setIsLoading] = useState(false); const [color, setColor] = useState(""); const [clipboardLock, setClipboardLock] = useState(false); const [gameDetails, setGameDetails] = useState(null); @@ -69,6 +73,7 @@ export function GameDetails() { }, [getGame, gameDownloading?.id]); useEffect(() => { + setIsLoading(true); dispatch(setHeaderTitle("")); window.electron @@ -87,6 +92,9 @@ export function GameDetails() { setGameDetails(result); dispatch(setHeaderTitle(result.name)); + }) + .finally(() => { + setIsLoading(false); }); getGame(); @@ -143,7 +151,7 @@ export function GameDetails() { }; return ( - <> + {gameDetails && ( )} -
-
- -
-
- + {isLoading ? ( + + ) : ( +
+
+ +
+
+ +
-
- setShowRepacksModal(true)} - getGame={getGame} - /> + setShowRepacksModal(true)} + getGame={getGame} + /> -
-
-
-
-

- {t("release_date", { - date: gameDetails?.release_date.date, - })} -

-

- {t("publisher", { publisher: gameDetails?.publishers[0] })} -

-
- - +
+
+
+
+

+ {t("release_date", { + date: gameDetails?.release_date.date, + })} +

+

+ {t("publisher", { publisher: gameDetails?.publishers[0] })} +

+
+ + +
+ +
-
-
- -
- - -
-

{t("requirements")}

-
+
+ -
- - +

{t("requirements")}

+
+ +
+ + +
+
-
-
-
- + + )} +
); }