diff --git a/components/SearchModal/index.scss b/components/SearchModal/index.scss index d9b5e5aa..35862d4f 100644 --- a/components/SearchModal/index.scss +++ b/components/SearchModal/index.scss @@ -3,38 +3,52 @@ flex-direction: column; min-height: 431px; width: 600px; + height: 480px; gap: 0; padding: 0; #Header { - border-top-left-radius: $unit; - border-top-right-radius: $unit; + border-bottom: 1px solid transparent; display: flex; - gap: $unit * 2.5; - margin: 0; - padding: ($unit * 3) ($unit * 3) ($unit) ($unit * 3); - position: sticky; - top: 0; + flex-direction: column; + gap: $unit; + padding-bottom: $unit * 2; - button { - background: transparent; - border: none; - height: 42px; - padding: 0; + &.scrolled { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); } - label { - width: 100%; + #Bar { + border-top-left-radius: $unit; + border-top-right-radius: $unit; + display: flex; + gap: $unit * 2.5; + margin: 0; + padding: ($unit * 3) ($unit * 3) 0 ($unit * 3); + position: sticky; + top: 0; - .Input { - background: $grey-90; + button { + background: transparent; border: none; - border-radius: calc($unit / 2); - box-sizing: border-box; - font-size: $font-regular; - padding: $unit * 1.5; - text-align: left; + height: 42px; + padding: 0; + } + + label { width: 100%; + + .Input { + background: $grey-90; + border: none; + border-radius: calc($unit / 2); + box-sizing: border-box; + font-size: $font-regular; + padding: $unit * 1.5; + text-align: left; + width: 100%; + } } } } @@ -45,6 +59,23 @@ padding: 0 ($unit * 1.5); overflow-y: scroll; + h5.total { + font-size: $font-regular; + font-weight: $normal; + color: $grey-40; + padding: calc($unit / 2) ($unit * 1.5); + } + + .footer { + align-items: center; + display: flex; + color: $grey-60; + font-size: $font-regular; + font-weight: $normal; + height: $unit * 10; + justify-content: center; + } + .WeaponResult:last-child { margin-bottom: $unit * 1.5; } diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index ea011f7d..60d5089a 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -1,12 +1,18 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' import { useSnapshot } from 'valtio' +import { useTranslation } from 'react-i18next' +import InfiniteScroll from 'react-infinite-scroll-component' import { appState } from '~utils/appState' import api from '~utils/api' import * as Dialog from '@radix-ui/react-dialog' +import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar' +import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar' +import SummonSearchFilterBar from '~components/SummonSearchFilterBar' + import CharacterResult from '~components/CharacterResult' import WeaponResult from '~components/WeaponResult' import SummonResult from '~components/SummonResult' @@ -28,17 +34,23 @@ const SearchModal = (props: Props) => { const router = useRouter() const locale = router.locale + const { t } = useTranslation('common') + let searchInput = React.createRef() + let scrollContainer = React.createRef() const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>() + const [filters, setFilters] = useState<{ [key: string]: number[] }>() const [open, setOpen] = useState(false) const [query, setQuery] = useState('') - const [results, setResults] = useState({}) - const [loading, setLoading] = useState(false) - const [message, setMessage] = useState('') - const [totalResults, setTotalResults] = useState(0) + const [results, setResults] = useState<(Weapon | Summon | Character)[]>([]) - useEffect(() => { + // Pagination states + const [recordCount, setRecordCount] = useState(0) + const [currentPage, setCurrentPage] = useState(1) + const [totalPages, setTotalPages] = useState(1) + + useEffect(() => { setObjects(grid[props.object]) }, [grid, props.object]) @@ -47,50 +59,46 @@ const SearchModal = (props: Props) => { searchInput.current.focus() }, [searchInput]) - useEffect(() => { - if (query.length > 1) - fetchResults() - }, [query]) - function inputChanged(event: React.ChangeEvent) { const text = event.target.value if (text.length) { setQuery(text) - setLoading(true) - setMessage('') } else { setQuery('') - setResults({}) - setMessage('') } } - function fetchResults() { - // Exclude objects in grid from search results - // unless the object is in the position that the user clicked - // so that users can replace object versions with - // compatible other objects. - const excludes = (objects) ? Object.values(objects) - .filter(filterExclusions) - .map((o) => { return (o.object) ? o.object.name.en : undefined }).join(',') : '' + function fetchResults({ replace = false }: { replace?: boolean }) { + api.search({ + object: props.object, + query: query, + filters: filters, + locale: locale, + page: currentPage + }).then(response => { + setTotalPages(response.data.total_pages) + setRecordCount(response.data.count) - api.search(props.object, query, excludes, locale) - .then(response => { - setResults(response.data) - setTotalResults(response.data.length) - setLoading(false) - }) - .catch(error => { - setMessage(error) - setLoading(false) - }) + if (replace) { + replaceResults(response.data.count, response.data.results) + } else { + appendResults(response.data.results) + } + }).catch(error => { + console.error(error) + }) } - function filterExclusions(gridObject: GridCharacter | GridWeapon | GridSummon) { - if (objects && gridObject.object && - gridObject.object.granblue_id === objects[props.fromPosition]?.object.granblue_id) - return null - else return gridObject + function replaceResults(count: number, list: Weapon[] | Summon[] | Character[]) { + if (count > 0) { + setResults(list) + } else { + setResults([]) + } + } + + function appendResults(list: Weapon[] | Summon[] | Character[]) { + setResults([...results, ...list]) } function sendData(result: Character | Weapon | Summon) { @@ -98,106 +106,160 @@ const SearchModal = (props: Props) => { setOpen(false) } + function receiveFilters(filters: { [key: string]: number[] }) { + setCurrentPage(1) + setResults([]) + setFilters(filters) + } + + useEffect(() => { + // Current page changed + if (open && currentPage > 1) { + fetchResults({ replace: false }) + } else if (open && currentPage == 1) { + fetchResults({ replace: true }) + } + }, [currentPage]) + + useEffect(() => { + // Filters changed + if (open) { + setCurrentPage(1) + fetchResults({ replace: true }) + } + }, [filters]) + + useEffect(() => { + // Query changed + if (open && query.length != 1) { + setCurrentPage(1) + fetchResults({ replace: true }) + } + }, [query]) + function renderResults() { + let jsx + switch(props.object) { case 'weapons': - return renderWeaponSearchResults(results) + jsx = renderWeaponSearchResults() break case 'summons': - return renderSummonSearchResults(results) + jsx = renderSummonSearchResults(results) break case 'characters': - return renderCharacterSearchResults(results) + jsx = renderCharacterSearchResults(results) break } - } - - function renderWeaponSearchResults(results: { [key: string]: any }) { - const elements = results.map((result: Weapon) => { - return { sendData(result) }} - /> - }) - - return (
    {elements}
) - } - - function renderSummonSearchResults(results: { [key: string]: any }) { - const elements = results.map((result: Summon) => { - return { sendData(result) }} - /> - }) - - return (
    {elements}
) - } - - function renderCharacterSearchResults(results: { [key: string]: any }) { - const elements = results.map((result: Character) => { - return { sendData(result) }} - /> - }) - - return (
    {elements}
) - } - - function renderEmptyState() { - let string = '' - - if (query === '') { - string = `No ${props.object}` - } else if (query.length < 2) { - string = `Type at least 2 characters` - } else { - string = `No results found for '${query}'` - } return ( -
-

{string}

-
+ 0) ? results.length : 0} + next={ () => setCurrentPage(currentPage + 1) } + hasMore={totalPages > currentPage} + scrollableTarget="Results" + loader={
Loading...
}> + {jsx} +
) } + + function renderWeaponSearchResults() { + let jsx: React.ReactNode + + const castResults: Weapon[] = results as Weapon[] + if (castResults && Object.keys(castResults).length > 0) { + jsx = castResults.map((result: Weapon) => { + return { sendData(result) }} + /> + }) + } + + return jsx + } + + function renderSummonSearchResults(results: { [key: string]: any }) { + let jsx: React.ReactNode + + const castResults: Summon[] = results as Summon[] + if (castResults && Object.keys(castResults).length > 0) { + jsx = castResults.map((result: Summon) => { + return { sendData(result) }} + /> + }) + } + + return jsx + } + + function renderCharacterSearchResults(results: { [key: string]: any }) { + let jsx: React.ReactNode + + const castResults: Character[] = results as Character[] + if (castResults && Object.keys(castResults).length > 0) { + jsx = castResults.map((result: Character) => { + return { sendData(result) }} + /> + }) + } + + return jsx + } - function resetAndClose() { - setQuery('') - setResults({}) - setOpen(true) + function openChange() { + if (open) { + setQuery('') + setResults([]) + setOpen(false) + } else { + setOpen(true) + } } return ( - + {props.children} + +
+
{t('search.result_count', { "record_count": recordCount })}
+ { (open) ? renderResults() : ''}
- { ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) }