mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-15 16:33:02 -03:00
feat: Added the game's supported languages. Added the game's lowest historical price and a link to see the price in various stores (keyshop and official store)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,55 +1,66 @@
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context/game-details/game-details.context";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
|
||||
export function GameLanguageSection() {
|
||||
const { t } = useTranslation("game_details");
|
||||
const { shopDetails, objectId } = useContext(gameDetailsContext);
|
||||
|
||||
const getLanguages = () => {
|
||||
let languages = shopDetails?.supported_languages;
|
||||
if (!languages) return [];
|
||||
languages = languages?.split('<br>')[0];
|
||||
let arrayIdiomas = languages?.split(',')
|
||||
let listLanguages: { language: string; caption: string; audio: string }[] = [];
|
||||
arrayIdiomas?.forEach((lang) => {
|
||||
const objectLanguage = {
|
||||
language: lang.replace("<strong>*</strong>", ""),
|
||||
caption: "✔",
|
||||
audio: lang.includes("*") ? "✔" : "" };
|
||||
listLanguages.push(objectLanguage);
|
||||
})
|
||||
return listLanguages;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SidebarSection title={t("language")}>
|
||||
<div>
|
||||
<h4>{t("supported_languages")}</h4>
|
||||
<table className="table-languages">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("language")}</th>
|
||||
<th>{t("caption")}</th>
|
||||
<th>{t("audio")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{getLanguages().map((lang) => (
|
||||
<tr key={lang.language}>
|
||||
<td>{lang.language}</td>
|
||||
<td>{lang.caption}</td>
|
||||
<td>{lang.audio}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<a target="_blank" rel="noopener noreferrer" className="list__item" href={`https://store.steampowered.com/app/${objectId}`}>Link Steam</a>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
);
|
||||
}
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context/game-details/game-details.context";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
|
||||
export function GameLanguageSection() {
|
||||
const { t } = useTranslation("game_details");
|
||||
const { shopDetails, objectId } = useContext(gameDetailsContext);
|
||||
|
||||
const getLanguages = () => {
|
||||
let languages = shopDetails?.supported_languages;
|
||||
if (!languages) return [];
|
||||
languages = languages?.split("<br>")[0];
|
||||
const arrayIdiomas = languages?.split(",");
|
||||
const listLanguages: {
|
||||
language: string;
|
||||
caption: string;
|
||||
audio: string;
|
||||
}[] = [];
|
||||
arrayIdiomas?.forEach((lang) => {
|
||||
const objectLanguage = {
|
||||
language: lang.replace("<strong>*</strong>", ""),
|
||||
caption: "✔",
|
||||
audio: lang.includes("*") ? "✔" : "",
|
||||
};
|
||||
listLanguages.push(objectLanguage);
|
||||
});
|
||||
return listLanguages;
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarSection title={t("language")}>
|
||||
<div>
|
||||
<h4>{t("supported_languages")}</h4>
|
||||
<table className="table-languages">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("language")}</th>
|
||||
<th>{t("caption")}</th>
|
||||
<th>{t("audio")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{getLanguages().map((lang) => (
|
||||
<tr key={lang.language}>
|
||||
<td>{lang.language}</td>
|
||||
<td>{lang.caption}</td>
|
||||
<td>{lang.audio}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="list__item"
|
||||
href={`https://store.steampowered.com/app/${objectId}`}
|
||||
>
|
||||
Link Steam
|
||||
</a>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,86 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context/game-details/game-details.context";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
|
||||
export function GamePricesSection() {
|
||||
const userPreferences = useAppSelector((state) => state.userPreferences.value);
|
||||
const { t } = useTranslation("game_details");
|
||||
const [priceData, setPriceData] = useState<any>(null);
|
||||
const [isLoadingPrices, setIsLoadingPrices] = useState(false);
|
||||
const { objectId } = useContext(gameDetailsContext);
|
||||
|
||||
const fetchGamePrices = useCallback(async (steamAppId: string) => {
|
||||
setIsLoadingPrices(true);
|
||||
try {
|
||||
const apiKey = userPreferences?.ggDealsApiKey || import.meta.env.VITE_GG_DEALS_API_KEY;
|
||||
if (!apiKey) {
|
||||
setPriceData(null);
|
||||
setIsLoadingPrices(false);
|
||||
return;
|
||||
}
|
||||
const url = `${import.meta.env.VITE_GG_DEALS_API_URL}/?ids=${steamAppId}&key=${apiKey}®ion=br`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
setPriceData(data.data?.[steamAppId] ?? null);
|
||||
} catch (error) {
|
||||
setPriceData(null);
|
||||
} finally {
|
||||
setIsLoadingPrices(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (objectId) {
|
||||
fetchGamePrices(objectId.toString());
|
||||
}
|
||||
}, [objectId, fetchGamePrices]);
|
||||
|
||||
return (
|
||||
<SidebarSection title={t("prices")}>
|
||||
{isLoadingPrices ? (
|
||||
<div>{t("loading")}</div>
|
||||
) : priceData ? (
|
||||
<div>
|
||||
<ul className="">
|
||||
<li><b>{t("retail_price")}</b>: {t("currency_symbol")}{priceData.prices.currentRetail}</li>
|
||||
<li><b>{t("keyshop_price")}</b>: {t("currency_symbol")}{priceData.prices.currentKeyshops}</li>
|
||||
<li><b>{t("historical_retail")}</b>: {t("currency_symbol")}{priceData.prices.historicalRetail}</li>
|
||||
<li><b>{t("historical_keyshop")}</b>: {t("currency_symbol")}{priceData.prices.historicalKeyshops}</li>
|
||||
<li><a href={priceData.url} target="_blank" rel="noopener noreferrer" className="list__item">clique para ver todos os preços</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("no_prices_found")}</div>
|
||||
)}
|
||||
</SidebarSection>
|
||||
);
|
||||
}
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context/game-details/game-details.context";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
|
||||
export function GamePricesSection() {
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
const { t } = useTranslation("game_details");
|
||||
const [priceData, setPriceData] = useState<any>(null);
|
||||
const [isLoadingPrices, setIsLoadingPrices] = useState(false);
|
||||
const { objectId } = useContext(gameDetailsContext);
|
||||
|
||||
const fetchGamePrices = useCallback(async (steamAppId: string) => {
|
||||
setIsLoadingPrices(true);
|
||||
try {
|
||||
const apiKey =
|
||||
userPreferences?.ggDealsApiKey || import.meta.env.VITE_GG_DEALS_API_KEY;
|
||||
if (!apiKey) {
|
||||
setPriceData(null);
|
||||
setIsLoadingPrices(false);
|
||||
return;
|
||||
}
|
||||
const url = `${import.meta.env.VITE_GG_DEALS_API_URL}/?ids=${steamAppId}&key=${apiKey}®ion=br`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
setPriceData(data.data?.[steamAppId] ?? null);
|
||||
} catch (error) {
|
||||
setPriceData(null);
|
||||
} finally {
|
||||
setIsLoadingPrices(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (objectId) {
|
||||
fetchGamePrices(objectId.toString());
|
||||
}
|
||||
}, [objectId, fetchGamePrices]);
|
||||
|
||||
return (
|
||||
<SidebarSection title={t("prices")}>
|
||||
{isLoadingPrices ? (
|
||||
<div>{t("loading")}</div>
|
||||
) : priceData ? (
|
||||
<div>
|
||||
<ul className="">
|
||||
<li>
|
||||
<b>{t("retail_price")}</b>: {t("currency_symbol")}
|
||||
{priceData.prices.currentRetail}
|
||||
</li>
|
||||
<li>
|
||||
<b>{t("keyshop_price")}</b>: {t("currency_symbol")}
|
||||
{priceData.prices.currentKeyshops}
|
||||
</li>
|
||||
<li>
|
||||
<b>{t("historical_retail")}</b>: {t("currency_symbol")}
|
||||
{priceData.prices.historicalRetail}
|
||||
</li>
|
||||
<li>
|
||||
<b>{t("historical_keyshop")}</b>: {t("currency_symbol")}
|
||||
{priceData.prices.historicalKeyshops}
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={priceData.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="list__item"
|
||||
>
|
||||
clique para ver todos os preços
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("no_prices_found")}</div>
|
||||
)}
|
||||
</SidebarSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,217 +1,218 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.content-sidebar {
|
||||
border-left: solid 1px globals.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement {
|
||||
&__button-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__details {
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
line-height: 22px;
|
||||
font-size: globals.$body-font-size;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.how-long-to-beat {
|
||||
&__categories-list {
|
||||
margin: 0;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 20%,
|
||||
rgb(255 255 255 / 2%) 100%
|
||||
);
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
border: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
&__category-label {
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__category-label--bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__category-skeleton {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-radius: 4px;
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
&__section {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
justify-content: space-between;
|
||||
transition: max-height ease 0.5s;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&__category-title {
|
||||
font-size: globals.$small-font-size;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.1s;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-image {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
|
||||
&--locked {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscription-required-button {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: globals.$warning-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.achievements-placeholder {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.achievements-placeholder__blur {
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.table-languages {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
|
||||
th, td {
|
||||
padding: globals.$spacing-unit;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: globals.$small-font-size;
|
||||
color: globals.$muted-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
}
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.content-sidebar {
|
||||
border-left: solid 1px globals.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement {
|
||||
&__button-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__details {
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
line-height: 22px;
|
||||
font-size: globals.$body-font-size;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
color: globals.$body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__details-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: globals.$spacing-unit;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.how-long-to-beat {
|
||||
&__categories-list {
|
||||
margin: 0;
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 20%,
|
||||
rgb(255 255 255 / 2%) 100%
|
||||
);
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
|
||||
border: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
&__category-label {
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__category-label--bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__category-skeleton {
|
||||
border: solid 1px globals.$border-color;
|
||||
border-radius: 4px;
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
&__section {
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
justify-content: space-between;
|
||||
transition: max-height ease 0.5s;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
&__category-title {
|
||||
font-size: globals.$small-font-size;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding: calc(globals.$spacing-unit * 2);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.1s;
|
||||
color: globals.$muted-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
padding: globals.$spacing-unit;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-image {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
|
||||
&--locked {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscription-required-button {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: calc(globals.$spacing-unit / 2);
|
||||
color: globals.$warning-color;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.achievements-placeholder {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.achievements-placeholder__blur {
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.table-languages {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: globals.$spacing-unit;
|
||||
border-bottom: solid 1px globals.$border-color;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: globals.$small-font-size;
|
||||
color: globals.$muted-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: globals.$body-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,273 +1,273 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import type {
|
||||
HowLongToBeatCategory,
|
||||
SteamAppDetails,
|
||||
UserAchievement,
|
||||
} from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||
import {
|
||||
CloudOfflineIcon,
|
||||
DownloadIcon,
|
||||
LockIcon,
|
||||
PeopleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./sidebar.scss";
|
||||
import { GamePricesSection } from "./game-prices-section";
|
||||
import { GameLanguageSection } from "./game-language-section";
|
||||
|
||||
const achievementsPlaceholder: UserAchievement[] = [
|
||||
{
|
||||
displayName: "Timber!!",
|
||||
name: "1",
|
||||
hidden: false,
|
||||
description: "Chop down your first tree.",
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
|
||||
unlocked: true,
|
||||
unlockTime: Date.now(),
|
||||
},
|
||||
{
|
||||
displayName: "Supreme Helper Minion!",
|
||||
name: "2",
|
||||
hidden: false,
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
},
|
||||
{
|
||||
displayName: "Feast of Midas",
|
||||
name: "3",
|
||||
hidden: false,
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
}>({ isLoading: true, data: null });
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, objectId, shop, stats, achievements } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
const { t } = useTranslation("game_details");
|
||||
const { formatDateTime } = useDate();
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
useEffect(() => {
|
||||
if (objectId) {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
|
||||
howLongToBeatEntriesTable
|
||||
.where({ shop, objectId })
|
||||
.first()
|
||||
.then(async (cachedHowLongToBeat) => {
|
||||
if (cachedHowLongToBeat) {
|
||||
setHowLongToBeat({
|
||||
isLoading: false,
|
||||
data: cachedHowLongToBeat.categories,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const howLongToBeat = await window.electron.getHowLongToBeat(
|
||||
objectId,
|
||||
shop
|
||||
);
|
||||
|
||||
if (howLongToBeat) {
|
||||
howLongToBeatEntriesTable.add({
|
||||
objectId,
|
||||
shop: "steam",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
categories: howLongToBeat,
|
||||
});
|
||||
}
|
||||
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
} catch (err) {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [objectId, shop, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className="content-sidebar">
|
||||
<GameLanguageSection />
|
||||
<GamePricesSection />
|
||||
|
||||
{userDetails === null && (
|
||||
<SidebarSection title={t("achievements")}>
|
||||
<div className="achievements-placeholder">
|
||||
<LockIcon size={36} />
|
||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||
</div>
|
||||
<ul className="list achievements-placeholder__blur">
|
||||
{achievementsPlaceholder.map((achievement) => (
|
||||
<li key={achievement.name}>
|
||||
<div className="list__item">
|
||||
<img
|
||||
className={`list__item-image achievements-placeholder__blur ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<small>
|
||||
{achievement.unlockTime != null &&
|
||||
formatDateTime(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{userDetails && achievements && achievements.length > 0 && (
|
||||
<SidebarSection
|
||||
title={t("achievements_count", {
|
||||
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||
achievementsCount: achievements.length,
|
||||
})}
|
||||
>
|
||||
<ul className="list">
|
||||
{!hasActiveSubscription && (
|
||||
<button
|
||||
className="subscription-required-button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
<CloudOfflineIcon size={16} />
|
||||
<span>{t("achievements_not_sync")}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{achievements.slice(0, 4).map((achievement) => (
|
||||
<li key={achievement.displayName}>
|
||||
<Link
|
||||
to={buildGameAchievementPath({
|
||||
shop: shop,
|
||||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
})}
|
||||
className="list__item"
|
||||
title={achievement.description}
|
||||
>
|
||||
<img
|
||||
className={`list__item-image ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<small>
|
||||
{achievement.unlockTime != null &&
|
||||
formatDateTime(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<Link
|
||||
to={buildGameAchievementPath({
|
||||
shop: shop,
|
||||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
})}
|
||||
>
|
||||
{t("see_all_achievements")}
|
||||
</Link>
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{stats && (
|
||||
<SidebarSection title={t("stats")}>
|
||||
<div className="stats__section">
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<DownloadIcon size={18} />
|
||||
{t("download_count")}
|
||||
</p>
|
||||
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||
</div>
|
||||
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<PeopleIcon size={18} />
|
||||
{t("player_count")}
|
||||
</p>
|
||||
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
<HowLongToBeatSection
|
||||
howLongToBeatData={howLongToBeat.data}
|
||||
isLoading={howLongToBeat.isLoading}
|
||||
/>
|
||||
|
||||
<SidebarSection title={t("requirements")}>
|
||||
<div className="requirement__button-container">
|
||||
<Button
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("minimum")}
|
||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||
>
|
||||
{t("minimum")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("recommended")}
|
||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||
>
|
||||
{t("recommended")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="requirement__details"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
t(`no_${activeRequirement}_requirements`, {
|
||||
gameTitle,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</SidebarSection>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import type {
|
||||
HowLongToBeatCategory,
|
||||
SteamAppDetails,
|
||||
UserAchievement,
|
||||
} from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||
import {
|
||||
CloudOfflineIcon,
|
||||
DownloadIcon,
|
||||
LockIcon,
|
||||
PeopleIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import "./sidebar.scss";
|
||||
import { GamePricesSection } from "./game-prices-section";
|
||||
import { GameLanguageSection } from "./game-language-section";
|
||||
|
||||
const achievementsPlaceholder: UserAchievement[] = [
|
||||
{
|
||||
displayName: "Timber!!",
|
||||
name: "1",
|
||||
hidden: false,
|
||||
description: "Chop down your first tree.",
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
|
||||
unlocked: true,
|
||||
unlockTime: Date.now(),
|
||||
},
|
||||
{
|
||||
displayName: "Supreme Helper Minion!",
|
||||
name: "2",
|
||||
hidden: false,
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
},
|
||||
{
|
||||
displayName: "Feast of Midas",
|
||||
name: "3",
|
||||
hidden: false,
|
||||
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
|
||||
icongray:
|
||||
"https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
}>({ isLoading: true, data: null });
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, objectId, shop, stats, achievements } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
const { t } = useTranslation("game_details");
|
||||
const { formatDateTime } = useDate();
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
useEffect(() => {
|
||||
if (objectId) {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
|
||||
howLongToBeatEntriesTable
|
||||
.where({ shop, objectId })
|
||||
.first()
|
||||
.then(async (cachedHowLongToBeat) => {
|
||||
if (cachedHowLongToBeat) {
|
||||
setHowLongToBeat({
|
||||
isLoading: false,
|
||||
data: cachedHowLongToBeat.categories,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const howLongToBeat = await window.electron.getHowLongToBeat(
|
||||
objectId,
|
||||
shop
|
||||
);
|
||||
|
||||
if (howLongToBeat) {
|
||||
howLongToBeatEntriesTable.add({
|
||||
objectId,
|
||||
shop: "steam",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
categories: howLongToBeat,
|
||||
});
|
||||
}
|
||||
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
} catch (err) {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [objectId, shop, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className="content-sidebar">
|
||||
<GameLanguageSection />
|
||||
<GamePricesSection />
|
||||
|
||||
{userDetails === null && (
|
||||
<SidebarSection title={t("achievements")}>
|
||||
<div className="achievements-placeholder">
|
||||
<LockIcon size={36} />
|
||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||
</div>
|
||||
<ul className="list achievements-placeholder__blur">
|
||||
{achievementsPlaceholder.map((achievement) => (
|
||||
<li key={achievement.name}>
|
||||
<div className="list__item">
|
||||
<img
|
||||
className={`list__item-image achievements-placeholder__blur ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<small>
|
||||
{achievement.unlockTime != null &&
|
||||
formatDateTime(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{userDetails && achievements && achievements.length > 0 && (
|
||||
<SidebarSection
|
||||
title={t("achievements_count", {
|
||||
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||
achievementsCount: achievements.length,
|
||||
})}
|
||||
>
|
||||
<ul className="list">
|
||||
{!hasActiveSubscription && (
|
||||
<button
|
||||
className="subscription-required-button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
<CloudOfflineIcon size={16} />
|
||||
<span>{t("achievements_not_sync")}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{achievements.slice(0, 4).map((achievement) => (
|
||||
<li key={achievement.displayName}>
|
||||
<Link
|
||||
to={buildGameAchievementPath({
|
||||
shop: shop,
|
||||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
})}
|
||||
className="list__item"
|
||||
title={achievement.description}
|
||||
>
|
||||
<img
|
||||
className={`list__item-image ${
|
||||
achievement.unlocked ? "" : "list__item-image--locked"
|
||||
}`}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<small>
|
||||
{achievement.unlockTime != null &&
|
||||
formatDateTime(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<Link
|
||||
to={buildGameAchievementPath({
|
||||
shop: shop,
|
||||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
})}
|
||||
>
|
||||
{t("see_all_achievements")}
|
||||
</Link>
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{stats && (
|
||||
<SidebarSection title={t("stats")}>
|
||||
<div className="stats__section">
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<DownloadIcon size={18} />
|
||||
{t("download_count")}
|
||||
</p>
|
||||
<p>{numberFormatter.format(stats?.downloadCount)}</p>
|
||||
</div>
|
||||
|
||||
<div className="stats__category">
|
||||
<p className="stats__category-title">
|
||||
<PeopleIcon size={18} />
|
||||
{t("player_count")}
|
||||
</p>
|
||||
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
<HowLongToBeatSection
|
||||
howLongToBeatData={howLongToBeat.data}
|
||||
isLoading={howLongToBeat.isLoading}
|
||||
/>
|
||||
|
||||
<SidebarSection title={t("requirements")}>
|
||||
<div className="requirement__button-container">
|
||||
<Button
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("minimum")}
|
||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||
>
|
||||
{t("minimum")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="requirement__button"
|
||||
onClick={() => setActiveRequirement("recommended")}
|
||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||
>
|
||||
{t("recommended")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="requirement__details"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
t(`no_${activeRequirement}_requirements`, {
|
||||
gameTitle,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</SidebarSection>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user