feat: updating play label on hero panel

This commit is contained in:
Hydra 2024-04-18 22:26:17 +01:00
parent 91b1341271
commit 96e11e6be9
No known key found for this signature in database
40 changed files with 2049 additions and 745 deletions

View file

@ -1,141 +1,113 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button, GameCard } from "@renderer/components";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { useTranslation } from "react-i18next";
import { Button, GameCard, Hero } from "@renderer/components";
import type { CatalogueCategory, CatalogueEntry } from "@types";
import type { CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./catalogue.css";
import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
import { useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "../home/home.css";
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
export function Catalogue() {
const dispatch = useAppDispatch();
const { t } = useTranslation("catalogue");
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
const [isLoading, setIsLoading] = useState(false);
const contentRef = useRef<HTMLElement>(null);
const cursorRef = useRef<number>(0);
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
const [searchParams] = useSearchParams();
const cursor = Number(searchParams.get("cursor") ?? 0);
const [catalogue, setCatalogue] = useState<
Record<CatalogueCategory, CatalogueEntry[]>
>({
trending: [],
recently_added: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setIsLoading(true);
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
});
}, []);
const currentCategory = searchParams.get("category") || categories[0];
const handleSelectCategory = (category: CatalogueCategory) => {
if (category !== currentCategory) {
getCatalogue(category);
navigate(`/?category=${category}`, { replace: true });
}
};
const getRandomGame = useCallback(() => {
setIsLoadingRandomGame(true);
window.electron
.getRandomGame()
.then((objectID) => {
randomGameObjectID.current = objectID;
})
.finally(() => {
setIsLoadingRandomGame(false);
});
}, []);
const handleRandomizerClick = () => {
const searchParams = new URLSearchParams({
fromRandomizer: "1",
});
navigate(
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
);
const handleGameClick = (game: CatalogueEntry) => {
dispatch(clearSearch());
navigate(`/game/${game.shop}/${game.objectID}`);
};
useEffect(() => {
if (contentRef.current) contentRef.current.scrollTop = 0;
setIsLoading(true);
getCatalogue(currentCategory as CatalogueCategory);
getRandomGame();
}, [getCatalogue, currentCategory, getRandomGame]);
setSearchResults([]);
window.electron
.getGames(24, cursor)
.then(({ results, cursor }) => {
return new Promise((resolve) => {
setTimeout(() => {
cursorRef.current = cursor;
setSearchResults(results);
resolve(null);
}, 500);
});
})
.finally(() => {
setIsLoading(false);
});
}, [dispatch, cursor, searchParams]);
const handleNextPage = () => {
const params = new URLSearchParams({
cursor: cursorRef.current.toString(),
});
navigate(`/catalogue?${params.toString()}`);
};
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<h2>{t("featured")}</h2>
<section
style={{
padding: `16px 32px`,
display: "flex",
width: "100%",
justifyContent: "space-between",
borderBottom: `1px solid ${vars.color.borderColor}`,
}}
>
<Button
onClick={() => navigate(-1)}
theme="outline"
disabled={cursor === 0 || isLoading}
>
<ArrowLeftIcon />
{t("previous_page")}
</Button>
<Hero />
<Button onClick={handleNextPage} theme="outline" disabled={isLoading}>
{t("next_page")}
<ArrowRightIcon />
</Button>
</section>
<section className={styles.catalogueHeader}>
<div className={styles.catalogueCategories}>
{categories.map((category) => (
<Button
key={category}
theme={currentCategory === category ? "primary" : "outline"}
onClick={() => handleSelectCategory(category)}
>
{t(category)}
</Button>
<section ref={contentRef} className={styles.content}>
<section className={styles.cards}>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))}
</div>
<Button
onClick={handleRandomizerClick}
theme="outline"
disabled={isLoadingRandomGame}
>
<div style={{ width: 16, height: 16, position: "relative" }}>
<Lottie
animationData={starsAnimation}
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
loop
/>
</div>
{t("surprise_me")}
</Button>
</section>
<h2>{t(currentCategory)}</h2>
<section className={styles.cards({})}>
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))
: catalogue[currentCategory as CatalogueCategory].map((result) => (
{!isLoading && searchResults.length > 0 && (
<>
{searchResults.map((game) => (
<GameCard
key={result.objectID}
game={result}
onClick={() =>
navigate(`/game/${result.shop}/${result.objectID}`)
}
key={game.objectID}
game={game}
onClick={() => handleGameClick(game)}
disabled={!game.repacks.length}
/>
))}
</>
)}
</section>
</section>
</SkeletonTheme>

View file

@ -217,16 +217,19 @@ export const howLongToBeatCategorySkeleton = style({
export const randomizerButton = style({
animationName: slideIn,
animationDuration: "0.4s",
animationDuration: "0.2s",
position: "fixed",
bottom: 26 + 16,
/* Bottom panel height + spacing */
bottom: `${26 + SPACING_UNIT * 2}px`,
/* Scroll bar + spacing */
right: `${9 + SPACING_UNIT * 2}px`,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
border: `solid 1px ${vars.color.borderColor}`,
backgroundColor: vars.color.darkBackground,
border: `solid 2px ${vars.color.borderColor}`,
backgroundColor: vars.color.background,
":hover": {
backgroundColor: vars.color.background,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 15px 5px",
opacity: 1,
opacity: "1",
},
":active": {
transform: "scale(0.98)",

View file

@ -104,7 +104,9 @@ export function HeroPanel({
window.electron
.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Game executable (.exe)", extensions: ["exe"] }],
filters: [
{ name: "Game executable (.exe)", extensions: ["exe", "app"] },
],
})
.then(({ filePaths }) => {
if (filePaths && filePaths.length > 0) {
@ -209,11 +211,15 @@ export function HeroPanel({
})}
</p>
<p>
{t("last_time_played", {
period: lastTimePlayed,
})}
</p>
{isGamePlaying ? (
<p>{t("playing_now")}</p>
) : (
<p>
{t("last_time_played", {
period: lastTimePlayed,
})}
</p>
)}
</>
);
}

View file

@ -1,6 +1,6 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT } from "@renderer/theme.css";
export const catalogueCategories = style({
display: "flex",
@ -23,12 +23,4 @@ export const cards = recipe({
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
},
variants: {
searching: {
true: {
pointerEvents: "none",
opacity: vars.opacity.disabled,
},
},
},
});

View file

@ -1,13 +1,12 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
export const catalogueCategories = style({
export const homeCategories = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const catalogueHeader = style({
export const homeHeader = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
justifyContent: "space-between",
@ -24,30 +23,20 @@ export const content = style({
overflowY: "auto",
});
export const cards = recipe({
base: {
display: "grid",
gridTemplateColumns: "repeat(1, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
"@media": {
"(min-width: 768px)": {
gridTemplateColumns: "repeat(2, 1fr)",
},
"(min-width: 1250px)": {
gridTemplateColumns: "repeat(3, 1fr)",
},
"(min-width: 1600px)": {
gridTemplateColumns: "repeat(4, 1fr)",
},
export const cards = style({
display: "grid",
gridTemplateColumns: "repeat(1, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
"@media": {
"(min-width: 768px)": {
gridTemplateColumns: "repeat(2, 1fr)",
},
},
variants: {
searching: {
true: {
pointerEvents: "none",
opacity: vars.opacity.disabled,
},
"(min-width: 1250px)": {
gridTemplateColumns: "repeat(3, 1fr)",
},
"(min-width: 1600px)": {
gridTemplateColumns: "repeat(4, 1fr)",
},
},
});

View file

@ -0,0 +1,143 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { Button, GameCard, Hero } from "@renderer/components";
import type { CatalogueCategory, CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./home.css";
import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
export function Home() {
const { t } = useTranslation("home");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
const [searchParams] = useSearchParams();
const [catalogue, setCatalogue] = useState<
Record<CatalogueCategory, CatalogueEntry[]>
>({
trending: [],
recently_added: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setIsLoading(true);
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
});
}, []);
const currentCategory = searchParams.get("category") || categories[0];
const handleSelectCategory = (category: CatalogueCategory) => {
if (category !== currentCategory) {
getCatalogue(category);
navigate(`/?category=${category}`, { replace: true });
}
};
const getRandomGame = useCallback(() => {
setIsLoadingRandomGame(true);
window.electron
.getRandomGame()
.then((objectID) => {
randomGameObjectID.current = objectID;
})
.finally(() => {
setIsLoadingRandomGame(false);
});
}, []);
const handleRandomizerClick = () => {
const searchParams = new URLSearchParams({
fromRandomizer: "1",
});
navigate(
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
);
};
useEffect(() => {
setIsLoading(true);
getCatalogue(currentCategory as CatalogueCategory);
getRandomGame();
}, [getCatalogue, currentCategory, getRandomGame]);
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<h2>{t("featured")}</h2>
<Hero />
<section className={styles.homeHeader}>
<div className={styles.homeCategories}>
{categories.map((category) => (
<Button
key={category}
theme={currentCategory === category ? "primary" : "outline"}
onClick={() => handleSelectCategory(category)}
>
{t(category)}
</Button>
))}
</div>
<Button
onClick={handleRandomizerClick}
theme="outline"
disabled={isLoadingRandomGame}
>
<div style={{ width: 16, height: 16, position: "relative" }}>
<Lottie
animationData={starsAnimation}
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
loop
/>
</div>
{t("surprise_me")}
</Button>
</section>
<h2>{t(currentCategory)}</h2>
<section className={styles.cards}>
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))
: catalogue[currentCategory as CatalogueCategory].map((result) => (
<GameCard
key={result.objectID}
game={result}
onClick={() =>
navigate(`/game/${result.shop}/${result.objectID}`)
}
/>
))}
</section>
</section>
</SkeletonTheme>
);
}

View file

@ -13,12 +13,12 @@ import { vars } from "@renderer/theme.css";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "./catalogue.css";
import * as styles from "./home.css";
export function SearchResults() {
const dispatch = useAppDispatch();
const { t } = useTranslation("catalogue");
const { t } = useTranslation("home");
const [searchParams] = useSearchParams();
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
@ -54,7 +54,7 @@ export function SearchResults() {
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<section className={styles.cards({ searching: false })}>
<section className={styles.cards}>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />

View file

@ -1,5 +1,6 @@
export * from "./catalogue/catalogue";
export * from "./home/home";
export * from "./game-details/game-details";
export * from "./downloads/downloads";
export * from "./catalogue/search-results";
export * from "./home/search-results";
export * from "./settings/settings";
export * from "./catalogue/catalogue";