From e4829405cc660c89aeaaef5384dcb4dc9066bb67 Mon Sep 17 00:00:00 2001 From: Biliane Silva <61093873+biliesilva@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:12:58 +0100 Subject: [PATCH 1/3] feat: add SearchBarDiscover component and integrate into TopNavBar --- .../UI/SearchBarDiscover/index.module.scss | 81 +++++++++ .../components/UI/SearchBarDiscover/index.tsx | 164 ++++++++++++++++++ .../components/UI/TopNavBar/index.tsx | 12 +- 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/frontend/components/UI/SearchBarDiscover/index.module.scss create mode 100644 src/frontend/components/UI/SearchBarDiscover/index.tsx diff --git a/src/frontend/components/UI/SearchBarDiscover/index.module.scss b/src/frontend/components/UI/SearchBarDiscover/index.module.scss new file mode 100644 index 000000000..d5f78e109 --- /dev/null +++ b/src/frontend/components/UI/SearchBarDiscover/index.module.scss @@ -0,0 +1,81 @@ +.SearchBar { + grid-area: search; + width: 48px; + position: relative; + display: inline-flex; + border-radius: var(--space-3xs); + padding: var(--space-3xs); + background: none; + transition: 250ms; + border-radius: 1000px; +} + +div.SearchBar:has(*:focus), +div.SearchBar:has(input:not(:placeholder-shown)) { + width: 500px; + background: var(--background-base); +} + +div.SearchBar:has(*:focus) > button, +div.SearchBar:has(input:not(:placeholder-shown)) > button { + border: transparent; +} + +.autoComplete { + position: absolute; + top: 100%; + max-height: 200px; + width: 100%; + background-color: var(--background-base); + overflow: auto; + list-style: none; + margin: -2px -4px; + display: none; + padding: var(--space-xs) var(--space-md); + text-align: left; + overflow-x: hidden; + z-index: 1000; + border-radius: var(--space-sm); +} + +.SearchBar:focus-within ul.autoComplete { + display: block; +} + +.autoComplete li { + padding: var(--space-sm); +} + +.autoComplete li:hover { + cursor: pointer; +} + +.searchButton { + padding: var(--space-2xs) var(--space-2xs) 0 var(--space-2xs); +} + +.clearSearchButton { + padding-right: var(--space-md); + transition: color 250ms; + background: transparent; + border: none; + color: var(--text-weak); +} + +.searchBarInput { + width: 100%; + appearance: none; + background: transparent; + color: var(--text-weak); + padding: 0 var(--space-lg); + border: none; + outline: none; + transition: color 250ms; +} + +@media screen and (max-width: 1280px) { + div.SearchBar:has(*:focus), + div.SearchBar:has(input:not(:placeholder-shown)) { + width: 243px; + } +} diff --git a/src/frontend/components/UI/SearchBarDiscover/index.tsx b/src/frontend/components/UI/SearchBarDiscover/index.tsx new file mode 100644 index 000000000..ec68be0e3 --- /dev/null +++ b/src/frontend/components/UI/SearchBarDiscover/index.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import './index.module.scss' +import { faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Images } from '@hyperplay/ui' +import TopNavBarStyles from '../TopNavBar/index.module.scss' +import Fuse from 'fuse.js' + +type ListingApi = { + project_meta?: { name?: string } + project_name?: string + id?: string + project_id?: string + account_name?: string + account_meta?: { name?: string } +} + +type GameResult = { + title: string + appId: string + storeUrl: string + accountName: string + projectName: string +} + +export default function SearchBarDiscover() { + const { t } = useTranslation() + const navigate = useNavigate() + const input = useRef(null) + const [filterText, setFilterText] = useState('') + const [allGames, setAllGames] = useState([]) + const [loading, setLoading] = useState(false) + + useEffect(() => { + let ignore = false + const fetchGames = async () => { + setLoading(true) + try { + const res = await fetch( + 'https://developers.hyperplay.xyz/api/v2/listings' + ) + const data = await res.json() + + const listings = Array.isArray(data) ? data : [] + const mapped: GameResult[] = listings + .map((listing: ListingApi) => ({ + title: + (listing.project_meta?.name ?? + listing.project_name ?? + listing.id) || + '', + appId: + (listing.project_id ?? listing.id ?? listing.project_name) || '', + storeUrl: + listing.account_name && listing.project_name + ? `/store/game/${listing.account_name}/${listing.project_name}` + : '', + accountName: + listing.account_name || listing.account_meta?.name || '', + projectName: listing.project_name || '' + })) + .filter( + (game) => + game.title && + game.title.trim() !== '' && + game.accountName && + game.projectName + ) + if (!ignore) { + setAllGames(mapped) + console.log('Total games loaded:', mapped.length) + console.log('Sample games:', mapped.slice(0, 3)) + } + } catch (e) { + if (!ignore) setAllGames([]) + } finally { + if (!ignore) setLoading(false) + } + } + fetchGames() + return () => { + ignore = true + } + }, []) + + const fuse = useMemo(() => { + return new Fuse(allGames, { + keys: ['title', 'accountName', 'projectName', 'appId'], + threshold: 0.4, + includeScore: true, + minMatchCharLength: 1, + ignoreLocation: true, + useExtendedSearch: true, + findAllMatches: true + }) + }, [allGames]) + + const filteredGames = useMemo(() => { + if (!filterText) return [] + const result = fuse.search(filterText) + + return result + .sort((a, b) => a.score! - b.score!) + .slice(0, 10) + .map((item) => item.item) + }, [fuse, filterText]) + + const showAutoComplete = filteredGames.length > 0 && filterText.length > 0 + + const onClear = useCallback(() => { + setFilterText('') + if (input.current) { + input.current.value = '' + input.current.focus() + } + }, [input]) + + function handleGameClick(game: GameResult) { + navigate( + `/store-page?store-url=https://store.hyperplay.xyz/game/${game.accountName}/${game.projectName}` + ) + } + + return ( +
+ + setFilterText(e.target.value)} + /> + {loading} + {showAutoComplete ? ( + <> +
    + {filteredGames.map((game) => ( +
  • handleGameClick(game)} + > + {game.title} +
  • + ))} +
+ + + ) : null} +
+ ) +} diff --git a/src/frontend/components/UI/TopNavBar/index.tsx b/src/frontend/components/UI/TopNavBar/index.tsx index c22c96561..27658e2ff 100644 --- a/src/frontend/components/UI/TopNavBar/index.tsx +++ b/src/frontend/components/UI/TopNavBar/index.tsx @@ -17,6 +17,7 @@ import { import webviewNavigationStore from 'frontend/store/WebviewNavigationStore' import { extractMainDomain } from '../../../helpers/extract-main-domain' import AppVersion from '../AppVersion' +import SearchBarDiscover from '../SearchBarDiscover' const TopNavBar = observer(() => { const { t } = useTranslation() @@ -51,6 +52,15 @@ const TopNavBar = observer(() => { return isActive ? activeStyle : inactiveStyle } + const getSearchBarType = () => { + if (pathname === '/library') { + return + } else if (pathname === '/hyperplaystore') { + return + } + return null + } + return (
@@ -102,7 +112,7 @@ const TopNavBar = observer(() => {
- {pathname === '/library' ? : null} + {getSearchBarType()} {showMetaMaskBrowserSidebarLinks && (