From 05a31cf6a61118aa93c16d5a0f950eca5590fec2 Mon Sep 17 00:00:00 2001 From: Hydra Date: Sun, 14 Apr 2024 23:00:56 +0100 Subject: [PATCH] feat: adding better navigation with search results --- .prettierrc.js | 6 ++ src/locales/en/translation.json | 8 ++- src/locales/es/translation.json | 8 ++- src/locales/pt/translation.json | 8 ++- .../events/catalogue/get-how-long-to-beat.ts | 4 +- src/main/helpers/formatters.ts | 2 +- src/main/services/how-long-to-beat.ts | 16 ++--- src/renderer/app.tsx | 24 ++----- src/renderer/components/header/header.tsx | 4 +- src/renderer/declaration.d.ts | 3 +- src/renderer/features/search-slice.ts | 13 +--- src/renderer/main.tsx | 2 +- .../pages/catalogue/search-results.tsx | 36 ++++++++-- .../pages/game-details/game-details.css.ts | 23 ++++++ .../pages/game-details/game-details.tsx | 70 ++++--------------- .../game-details/how-long-to-beat-section.tsx | 65 +++++++++++++++++ src/types/index.ts | 6 ++ 17 files changed, 187 insertions(+), 111 deletions(-) create mode 100644 .prettierrc.js create mode 100644 src/renderer/pages/game-details/how-long-to-beat-section.tsx diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..4bc87a04 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + semi: true, + trailingComma: "all", + singleQuote: false, + tabWidth: 2, +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 207613fd..432d4b0d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -57,7 +57,13 @@ "release_date": "Released in {{date}}", "publisher": "Published by {{publisher}}", "copy_link_to_clipboard": "Copy link", - "copied_link_to_clipboard": "Link copied" + "copied_link_to_clipboard": "Link copied", + "main_story": "Main story", + "main_plus_sides": "Main story + sides", + "completionist": "Completionist", + "all_styles": "All styles", + "hours": "hours", + "minutes": "minutes" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 0153de88..8e877103 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -57,7 +57,13 @@ "release_date": "Fecha de lanzamiento {{date}}", "publisher": "Publicado por {{publisher}}", "copy_link_to_clipboard": "Copiar enlace", - "copied_link_to_clipboard": "Enlace copiado" + "copied_link_to_clipboard": "Enlace copiado", + "main_story": "Historia principal", + "main_plus_sides": "Historia principal + extras", + "completionist": "Completista", + "all_styles": "Todo", + "hours": "horas", + "minutes": "minutos" }, "activation": { "title": "Activar Hydra", diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 722e7145..8f5a2e7c 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -57,7 +57,13 @@ "release_date": "Lançado em {{date}}", "publisher": "Publicado por {{publisher}}", "copy_link_to_clipboard": "Copiar link", - "copied_link_to_clipboard": "Link copiado" + "copied_link_to_clipboard": "Link copiado", + "main_story": "História principal", + "main_plus_sides": "Historia principal + extras", + "completionist": "Completista", + "all_styles": "Tudo", + "hours": "horas", + "minutes": "minutos" }, "activation": { "title": "Ativação", diff --git a/src/main/events/catalogue/get-how-long-to-beat.ts b/src/main/events/catalogue/get-how-long-to-beat.ts index 7d15a34e..e737f391 100644 --- a/src/main/events/catalogue/get-how-long-to-beat.ts +++ b/src/main/events/catalogue/get-how-long-to-beat.ts @@ -1,4 +1,4 @@ -import type { GameShop } from "@types"; +import type { GameShop, HowLongToBeatCategory } from "@types"; import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services"; import { registerEvent } from "../register-event"; @@ -8,7 +8,7 @@ const getHowLongToBeat = async ( objectID: string, _shop: GameShop, title: string -): Promise | null> => { +): Promise => { const response = await searchHowLongToBeat(title); const game = response.data.find( (game) => game.profile_steam === Number(objectID) diff --git a/src/main/helpers/formatters.ts b/src/main/helpers/formatters.ts index ee2f5238..75393146 100644 --- a/src/main/helpers/formatters.ts +++ b/src/main/helpers/formatters.ts @@ -8,7 +8,7 @@ export const removeSymbolsFromName = (name: string) => export const removeSpecialEditionFromName = (name: string) => name.replace( - /(The |Digital )?(Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year) Edition/g, + /(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/g, "" ); diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts index c6b42197..e8471a94 100644 --- a/src/main/services/how-long-to-beat.ts +++ b/src/main/services/how-long-to-beat.ts @@ -52,16 +52,14 @@ export const getHowLongToBeatGame = async (id: string) => { const $ul = document.querySelector(".shadow_shadow ul"); const $lis = Array.from($ul.children); - return $lis.reduce((prev, next) => { - const name = next.querySelector("h4").textContent; - const [, time] = Array.from((next as HTMLElement).classList); + return $lis.map(($li) => { + const title = $li.querySelector("h4").textContent; + const [, time] = Array.from(($li as HTMLElement).classList); return { - ...prev, - [name]: { - time: next.querySelector("h5").textContent, - color: classNameColor[time as keyof typeof classNameColor], - }, + title, + duration: $li.querySelector("h5").textContent, + color: classNameColor[time as keyof typeof classNameColor], }; - }, {}); + }); }; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 89bdaed2..c72ba0a4 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -12,15 +12,12 @@ import { import * as styles from "./app.css"; import { themeClass } from "./theme.css"; -import debounce from "lodash/debounce"; -import type { DebouncedFunc } from "lodash"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { setSearch, clearSearch, setUserPreferences, setRepackersFriendlyNames, - setSearchResults, } from "@renderer/features"; document.body.classList.add(themeClass); @@ -36,8 +33,6 @@ export function App() { const navigate = useNavigate(); const location = useLocation(); - const debouncedFunc = useRef void | null>>(null); - const search = useAppSelector((state) => state.search.value); useEffect(() => { @@ -61,7 +56,7 @@ export function App() { } addPacket(downloadProgress); - } + }, ); return () => { @@ -72,26 +67,17 @@ export function App() { const handleSearch = useCallback( (query: string) => { dispatch(setSearch(query)); - if (debouncedFunc.current) debouncedFunc.current.cancel(); if (query === "") { navigate(-1); return; } - if (location.pathname !== "/search") { - navigate("/search"); - } - - debouncedFunc.current = debounce(() => { - window.electron.searchGames(query).then((results) => { - dispatch(setSearchResults(results)); - }); - }, 300); - - debouncedFunc.current(); + navigate(`/search/${query}`, { + replace: location.pathname.startsWith("/search"), + }); }, - [dispatch, location.pathname, navigate] + [dispatch, location.pathname, navigate], ); const handleClear = useCallback(() => { diff --git a/src/renderer/components/header/header.tsx b/src/renderer/components/header/header.tsx index 9e07c4a4..f93bd9cc 100644 --- a/src/renderer/components/header/header.tsx +++ b/src/renderer/components/header/header.tsx @@ -17,7 +17,6 @@ export interface HeaderProps { const pathTitle: Record = { "/": "catalogue", "/downloads": "downloads", - "/search": "search_results", "/settings": "settings", }; @@ -38,12 +37,13 @@ export function Header({ onSearch, onClear, search }: HeaderProps) { const title = useMemo(() => { if (location.pathname.startsWith("/game")) return headerTitle; + if (location.pathname.startsWith("/search")) return t("search_results"); return t(pathTitle[location.pathname]); }, [location.pathname, headerTitle, t]); useEffect(() => { - if (search && location.pathname !== "/search") { + if (search && !location.pathname.startsWith("/search")) { dispatch(clearSearch()); } }, [location.pathname, search, dispatch]); diff --git a/src/renderer/declaration.d.ts b/src/renderer/declaration.d.ts index 9ed239e5..00377675 100644 --- a/src/renderer/declaration.d.ts +++ b/src/renderer/declaration.d.ts @@ -6,6 +6,7 @@ import type { TorrentProgress, ShopDetails, UserPreferences, + HowLongToBeatCategory, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -43,7 +44,7 @@ declare global { objectID: string, shop: GameShop, title: string - ) => Promise | null>; + ) => Promise; /* Library */ getLibrary: () => Promise; diff --git a/src/renderer/features/search-slice.ts b/src/renderer/features/search-slice.ts index 431a5974..2c064aa2 100644 --- a/src/renderer/features/search-slice.ts +++ b/src/renderer/features/search-slice.ts @@ -1,18 +1,12 @@ import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; -import type { CatalogueEntry } from "@types"; - interface SearchState { value: string; - results: CatalogueEntry[]; - isLoading: boolean; } const initialState: SearchState = { value: "", - results: [], - isLoading: false, }; export const searchSlice = createSlice({ @@ -20,17 +14,12 @@ export const searchSlice = createSlice({ initialState, reducers: { setSearch: (state, action: PayloadAction) => { - state.isLoading = true; state.value = action.payload; }, clearSearch: (state) => { state.value = ""; }, - setSearchResults: (state, action: PayloadAction) => { - state.isLoading = false; - state.results = action.payload; - }, }, }); -export const { setSearch, clearSearch, setSearchResults } = searchSlice.actions; +export const { setSearch, clearSearch } = searchSlice.actions; diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 4839740f..3bad1a79 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -45,7 +45,7 @@ const router = createHashRouter([ Component: GameDetails, }, { - path: "/search", + path: "/search/:query", Component: SearchResults, }, { diff --git a/src/renderer/pages/catalogue/search-results.tsx b/src/renderer/pages/catalogue/search-results.tsx index 46a92376..b6ec3856 100644 --- a/src/renderer/pages/catalogue/search-results.tsx +++ b/src/renderer/pages/catalogue/search-results.tsx @@ -3,16 +3,26 @@ import { GameCard } from "@renderer/components"; import type { CatalogueEntry } from "@types"; +import debounce from "lodash/debounce"; +import type { DebouncedFunc } from "lodash"; + import * as styles from "./catalogue.css"; -import { useNavigate } from "react-router-dom"; -import { useAppDispatch, useAppSelector } from "@renderer/hooks"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAppDispatch } from "@renderer/hooks"; import { clearSearch } from "@renderer/features"; import { vars } from "@renderer/theme.css"; +import { useEffect, useRef, useState } from "react"; export function SearchResults() { - const { results, isLoading } = useAppSelector((state) => state.search); const dispatch = useAppDispatch(); + const { query } = useParams(); + + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const debouncedFunc = useRef void | null>>(null); + const navigate = useNavigate(); const handleGameClick = (game: CatalogueEntry) => { @@ -20,6 +30,24 @@ export function SearchResults() { navigate(`/game/${game.shop}/${game.objectID}`); }; + useEffect(() => { + setIsLoading(true); + if (debouncedFunc.current) debouncedFunc.current.cancel(); + + debouncedFunc.current = debounce(() => { + window.electron + .searchGames(query) + .then((results) => { + setSearchResults(results); + }) + .finally(() => { + setIsLoading(false); + }); + }, 300); + + debouncedFunc.current(); + }, [query, dispatch]); + return (
@@ -28,7 +56,7 @@ export function SearchResults() { ? Array.from({ length: 12 }).map((_, index) => ( )) - : results.map((game) => ( + : searchResults.map((game) => ( (null); - const [howLongToBeat, setHowLongToBeat] = useState | null>(null); - - console.log(howLongToBeat); + const [howLongToBeat, setHowLongToBeat] = useState< + HowLongToBeatCategory[] | null + >(null); const [game, setGame] = useState(null); const [activeRequirement, setActiveRequirement] = @@ -87,6 +90,7 @@ export function GameDetails() { getGame(); setHowLongToBeat(null); + setClipboardLock(false); }, [getGame, dispatch, navigate, objectID, i18n.language]); const handleCopyToClipboard = () => { @@ -213,55 +217,7 @@ export function GameDetails() {
- {howLongToBeat && ( - <> -
-

HowLongToBeat

-
- -
- {Object.entries(howLongToBeat).map(([key, value]) => ( -
-

- {key} -

-

- {value.time} -

-
- ))} -
- - )} +
= { + "Main Story": "main_story", + "Main + Sides": "main_plus_sides", + Completionist: "completionist", + "All Styles": "all_styles", +}; + +const durationTranslation: Record = { + Hours: "hours", + Mins: "minutes", +}; + +export interface HowLongToBeatSectionProps { + howLongToBeatData: HowLongToBeatCategory[] | null; +} + +export function HowLongToBeatSection({ + howLongToBeatData, +}: HowLongToBeatSectionProps) { + const { t } = useTranslation("game_details"); + + if (!howLongToBeatData) return null; + + const getDuration = (duration: string) => { + const [value, unit] = duration.split(" "); + return `${value} ${t(durationTranslation[unit])}`; + }; + + return ( + <> +
+

HowLongToBeat

+
+ +
    + {howLongToBeatData.map((category) => ( +
  • +

    + {titleTranslation[category.title] + ? t(titleTranslation[category.title]) + : category.title} +

    +

    + {getDuration(category.duration)} +

    +
  • + ))} +
+ + ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 42fb7850..580994ca 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -102,3 +102,9 @@ export interface UserPreferences { downloadNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean; } + +export interface HowLongToBeatCategory { + title: string; + duration: string; + color: string; +}