Merge pull request #27 from jedmund/search-filters
Implement filters in Search
This commit is contained in:
commit
cc01aa41c5
26 changed files with 1820 additions and 403 deletions
0
components/CharacterSearchFilterBar/index.scss
Normal file
0
components/CharacterSearchFilterBar/index.scss
Normal file
205
components/CharacterSearchFilterBar/index.tsx
Normal file
205
components/CharacterSearchFilterBar/index.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import SearchFilter from '~components/SearchFilter'
|
||||
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
|
||||
|
||||
import './index.scss'
|
||||
import { emptyElementState, emptyProficiencyState, emptyRarityState } from '~utils/emptyStates'
|
||||
import { elements, proficiencies, rarities } from '~utils/stateValues'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
}
|
||||
|
||||
const CharacterSearchFilterBar = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
const [proficiency1Menu, setProficiency1Menu] = useState(false)
|
||||
const [proficiency2Menu, setProficiency2Menu] = useState(false)
|
||||
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
|
||||
const [proficiency1State, setProficiency1State] = useState<ProficiencyState>(emptyProficiencyState)
|
||||
const [proficiency2State, setProficiency2State] = useState<ProficiencyState>(emptyProficiencyState)
|
||||
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setRarityMenu(false)
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(false)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
|
||||
function proficiency1MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(true)
|
||||
setProficiency2Menu(false)
|
||||
} else setProficiency1Menu(false)
|
||||
}
|
||||
|
||||
function proficiency2MenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiency1Menu(false)
|
||||
setProficiency2Menu(true)
|
||||
} else setProficiency2Menu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function handleProficiency1Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency1State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency1State(newProficiencyState)
|
||||
}
|
||||
|
||||
function handleProficiency2Change(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiency2State)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiency2State(newProficiencyState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedProficiency1Filters = Object.values(proficiency1State).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedProficiency2Filters = Object.values(proficiency2State).filter(x => x.checked).map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters,
|
||||
proficiency1: checkedProficiency1Filters,
|
||||
proficiency2: checkedProficiency2Filters
|
||||
}
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState, proficiency1State, proficiency2State])
|
||||
|
||||
function renderProficiencyFilter(proficiency: 1 | 2) {
|
||||
const onCheckedChange = (proficiency == 1) ? handleProficiency1Change : handleProficiency2Change
|
||||
const numSelected = (proficiency == 1)
|
||||
? Object.values(proficiency1State).map(x => x.checked).filter(Boolean).length
|
||||
: Object.values(proficiency2State).map(x => x.checked).filter(Boolean).length
|
||||
const open = (proficiency == 1) ? proficiency1Menu : proficiency2Menu
|
||||
const onOpenChange = (proficiency == 1) ? proficiency1MenuOpened : proficiency2MenuOpened
|
||||
|
||||
return (
|
||||
<SearchFilter
|
||||
label={`${t('filters.labels.proficiency')} ${proficiency}`}
|
||||
numSelected={numSelected}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}>
|
||||
<DropdownMenu.Label className="Label">{`${t('filters.labels.proficiency')} ${proficiency}`}</DropdownMenu.Label>
|
||||
<section>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked = (proficiency == 1)
|
||||
? proficiency1State[proficiencies[i]].checked
|
||||
: proficiency2State[proficiencies[i]].checked
|
||||
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i]}>
|
||||
{t(`proficiencies.${proficiencies[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
const checked = (proficiency == 1)
|
||||
? proficiency1State[proficiencies[i + (proficiencies.length / 2)]].checked
|
||||
: proficiency2State[proficiencies[i + (proficiencies.length / 2)]].checked
|
||||
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i + (proficiencies.length / 2)]}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
valueKey={proficiencies[i + (proficiencies.length / 2)]}>
|
||||
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
</section>
|
||||
</SearchFilter>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
{ renderProficiencyFilter(1) }
|
||||
{ renderProficiencyFilter(2) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterSearchFilterBar
|
||||
74
components/SearchFilter/index.scss
Normal file
74
components/SearchFilter/index.scss
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
.DropdownLabel {
|
||||
align-items: center;
|
||||
background: $grey-90;
|
||||
border: none;
|
||||
border-radius: $unit * 2;
|
||||
color: $grey-40;
|
||||
display: flex;
|
||||
gap: calc($unit / 2);
|
||||
flex-direction: row;
|
||||
padding: ($unit) ($unit * 2);
|
||||
|
||||
&:hover {
|
||||
background: $grey-80;
|
||||
color: $grey-00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: $grey-60;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
$diameter: 12px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
svg {
|
||||
transform: scale(0.85);
|
||||
|
||||
path {
|
||||
fill: $grey-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Dropdown {
|
||||
background: white;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc($unit / 2);
|
||||
padding: $unit;
|
||||
min-width: 120px;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
filter: drop-shadow(0px 0px 1px rgb(0 0 0 / 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.Group {
|
||||
flex: 1 1 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Label {
|
||||
color: $grey-60;
|
||||
font-size: $font-small;
|
||||
margin-bottom: calc($unit / 2);
|
||||
padding-left: calc($unit / 2);
|
||||
}
|
||||
}
|
||||
34
components/SearchFilter/index.tsx
Normal file
34
components/SearchFilter/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import ArrowIcon from '~public/icons/Arrow.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
open: boolean
|
||||
numSelected: number
|
||||
onOpenChange: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchFilter = (props: Props) => {
|
||||
return (
|
||||
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DropdownMenu.Trigger className="DropdownLabel">
|
||||
{props.label}
|
||||
<span className="count">{props.numSelected}</span>
|
||||
<span className="icon">
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="Dropdown" sideOffset={4}>
|
||||
{props.children}
|
||||
<DropdownMenu.Arrow />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilter
|
||||
41
components/SearchFilterCheckboxItem/index.scss
Normal file
41
components/SearchFilterCheckboxItem/index.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
.Item {
|
||||
align-items: center;
|
||||
border-radius: calc($unit / 2);
|
||||
color: $grey-40;
|
||||
font-size: $font-regular;
|
||||
line-height: 1.2;
|
||||
min-width: 100px;
|
||||
position: relative;
|
||||
padding: $unit;
|
||||
padding-left: $unit * 3;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state="checked"] {
|
||||
background: $grey-90;
|
||||
|
||||
svg {
|
||||
fill: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
.Indicator {
|
||||
$diameter: 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: calc($unit / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
svg {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
components/SearchFilterCheckboxItem/index.tsx
Normal file
34
components/SearchFilterCheckboxItem/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import CheckIcon from '~public/icons/Check.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
checked?: boolean
|
||||
valueKey: string
|
||||
onCheckedChange: (open: boolean, key: string) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchFilterCheckboxItem = (props: Props) => {
|
||||
function handleCheckedChange(checked: boolean) {
|
||||
props.onCheckedChange(checked, props.valueKey)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
className="Item"
|
||||
checked={props.checked || false}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
onSelect={ (event) => event.preventDefault() }>
|
||||
<DropdownMenu.ItemIndicator className="Indicator">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilterCheckboxItem
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>()
|
||||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
|
||||
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<HTMLInputElement>) {
|
||||
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 <WeaponResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="Results">{elements}</ul>)
|
||||
}
|
||||
|
||||
function renderSummonSearchResults(results: { [key: string]: any }) {
|
||||
const elements = results.map((result: Summon) => {
|
||||
return <SummonResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="Results">{elements}</ul>)
|
||||
}
|
||||
|
||||
function renderCharacterSearchResults(results: { [key: string]: any }) {
|
||||
const elements = results.map((result: Character) => {
|
||||
return <CharacterResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="Results">{elements}</ul>)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div id="NoResults">
|
||||
<h2>{string}</h2>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
dataLength={ (results && results.length > 0) ? results.length : 0}
|
||||
next={ () => setCurrentPage(currentPage + 1) }
|
||||
hasMore={totalPages > currentPage}
|
||||
scrollableTarget="Results"
|
||||
loader={<div className="footer">Loading...</div>}>
|
||||
{jsx}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
|
||||
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 <WeaponResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { 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 <SummonResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { 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 <CharacterResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function resetAndClose() {
|
||||
setQuery('')
|
||||
setResults({})
|
||||
setOpen(true)
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery('')
|
||||
setResults([])
|
||||
setOpen(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
{props.children}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Search Dialog">
|
||||
<div id="Header">
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={props.placeholderText}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
<Dialog.Close className="DialogClose" onClick={resetAndClose}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
<div id="Bar">
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={props.placeholderText}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
<Dialog.Close className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
{ (props.object === 'characters') ? <CharacterSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
{ (props.object === 'weapons') ? <WeaponSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
{ (props.object === 'summons') ? <SummonSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">{t('search.result_count', { "record_count": recordCount })}</h5>
|
||||
{ (open) ? renderResults() : ''}
|
||||
</div>
|
||||
{ ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) }
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
|
|
|
|||
0
components/SummonSearchFilterBar/index.scss
Normal file
0
components/SummonSearchFilterBar/index.scss
Normal file
105
components/SummonSearchFilterBar/index.tsx
Normal file
105
components/SummonSearchFilterBar/index.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import SearchFilter from '~components/SearchFilter'
|
||||
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
|
||||
|
||||
import './index.scss'
|
||||
import { emptyElementState, emptyRarityState } from '~utils/emptyStates'
|
||||
import { elements, rarities } from '~utils/stateValues'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
}
|
||||
|
||||
const SummonSearchFilterBar = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
|
||||
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
} else setRarityMenu(false)
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters
|
||||
}
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState])
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummonSearchFilterBar
|
||||
0
components/WeaponSearchFilterBar/index.scss
Normal file
0
components/WeaponSearchFilterBar/index.scss
Normal file
224
components/WeaponSearchFilterBar/index.tsx
Normal file
224
components/WeaponSearchFilterBar/index.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import SearchFilter from '~components/SearchFilter'
|
||||
import SearchFilterCheckboxItem from '~components/SearchFilterCheckboxItem'
|
||||
|
||||
import './index.scss'
|
||||
import { emptyElementState, emptyProficiencyState, emptyRarityState, emptyWeaponSeriesState } from '~utils/emptyStates'
|
||||
import { elements, proficiencies, rarities, weaponSeries } from '~utils/stateValues'
|
||||
|
||||
interface Props {
|
||||
sendFilters: (filters: { [key: string]: number[] }) => void
|
||||
}
|
||||
|
||||
const WeaponSearchFilterBar = (props: Props) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [rarityMenu, setRarityMenu] = useState(false)
|
||||
const [elementMenu, setElementMenu] = useState(false)
|
||||
const [proficiencyMenu, setProficiencyMenu] = useState(false)
|
||||
const [seriesMenu, setSeriesMenu] = useState(false)
|
||||
|
||||
const [rarityState, setRarityState] = useState<RarityState>(emptyRarityState)
|
||||
const [elementState, setElementState] = useState<ElementState>(emptyElementState)
|
||||
const [proficiencyState, setProficiencyState] = useState<ProficiencyState>(emptyProficiencyState)
|
||||
const [seriesState, setSeriesState] = useState<WeaponSeriesState>(emptyWeaponSeriesState)
|
||||
|
||||
function rarityMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(true)
|
||||
setElementMenu(false)
|
||||
setProficiencyMenu(false)
|
||||
setSeriesMenu(false)
|
||||
} else setRarityMenu(false)
|
||||
}
|
||||
|
||||
function elementMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(true)
|
||||
setProficiencyMenu(false)
|
||||
setSeriesMenu(false)
|
||||
} else setElementMenu(false)
|
||||
}
|
||||
|
||||
function proficiencyMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiencyMenu(true)
|
||||
setSeriesMenu(false)
|
||||
} else setProficiencyMenu(false)
|
||||
}
|
||||
|
||||
function seriesMenuOpened(open: boolean) {
|
||||
if (open) {
|
||||
setRarityMenu(false)
|
||||
setElementMenu(false)
|
||||
setProficiencyMenu(false)
|
||||
setSeriesMenu(true)
|
||||
} else setSeriesMenu(false)
|
||||
}
|
||||
|
||||
function handleRarityChange(checked: boolean, key: string) {
|
||||
let newRarityState = cloneDeep(rarityState)
|
||||
newRarityState[key].checked = checked
|
||||
setRarityState(newRarityState)
|
||||
}
|
||||
|
||||
function handleElementChange(checked: boolean, key: string) {
|
||||
let newElementState = cloneDeep(elementState)
|
||||
newElementState[key].checked = checked
|
||||
setElementState(newElementState)
|
||||
}
|
||||
|
||||
function handleProficiencyChange(checked: boolean, key: string) {
|
||||
let newProficiencyState = cloneDeep(proficiencyState)
|
||||
newProficiencyState[key].checked = checked
|
||||
setProficiencyState(newProficiencyState)
|
||||
}
|
||||
|
||||
function handleSeriesChange(checked: boolean, key: string) {
|
||||
let newSeriesState = cloneDeep(seriesState)
|
||||
newSeriesState[key].checked = checked
|
||||
setSeriesState(newSeriesState)
|
||||
}
|
||||
|
||||
function sendFilters() {
|
||||
const checkedRarityFilters = Object.values(rarityState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedElementFilters = Object.values(elementState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedProficiencyFilters = Object.values(proficiencyState).filter(x => x.checked).map((x, i) => x.id)
|
||||
const checkedSeriesFilters = Object.values(seriesState).filter(x => x.checked).map((x, i) => x.id)
|
||||
|
||||
const filters = {
|
||||
rarity: checkedRarityFilters,
|
||||
element: checkedElementFilters,
|
||||
proficiency1: checkedProficiencyFilters,
|
||||
series: checkedSeriesFilters
|
||||
}
|
||||
|
||||
props.sendFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sendFilters()
|
||||
}, [rarityState, elementState, proficiencyState, seriesState])
|
||||
|
||||
return (
|
||||
<div className="SearchFilterBar">
|
||||
<SearchFilter label={t('filters.labels.rarity')} numSelected={Object.values(rarityState).map(x => x.checked).filter(Boolean).length} open={rarityMenu} onOpenChange={rarityMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.rarity')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(rarities.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={rarities[i]}
|
||||
onCheckedChange={handleRarityChange}
|
||||
checked={rarityState[rarities[i]].checked}
|
||||
valueKey={rarities[i]}>
|
||||
{t(`rarities.${rarities[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.element')} numSelected={Object.values(elementState).map(x => x.checked).filter(Boolean).length} open={elementMenu} onOpenChange={elementMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.element')}</DropdownMenu.Label>
|
||||
{ Array.from(Array(elements.length)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={elements[i]}
|
||||
onCheckedChange={handleElementChange}
|
||||
checked={elementState[elements[i]].checked}
|
||||
valueKey={elements[i]}>
|
||||
{t(`elements.${elements[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.proficiency')} numSelected={Object.values(proficiencyState).map(x => x.checked).filter(Boolean).length} open={proficiencyMenu} onOpenChange={proficiencyMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.proficiency')}</DropdownMenu.Label>
|
||||
<section>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i]}
|
||||
onCheckedChange={handleProficiencyChange}
|
||||
checked={proficiencyState[proficiencies[i]].checked}
|
||||
valueKey={proficiencies[i]}>
|
||||
{t(`proficiencies.${proficiencies[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(proficiencies.length / 2)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={proficiencies[i + (proficiencies.length / 2)]}
|
||||
onCheckedChange={handleProficiencyChange}
|
||||
checked={proficiencyState[proficiencies[i + (proficiencies.length / 2)]].checked}
|
||||
valueKey={proficiencies[i + (proficiencies.length / 2)]}>
|
||||
{t(`proficiencies.${proficiencies[i + (proficiencies.length / 2)]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
</section>
|
||||
</SearchFilter>
|
||||
|
||||
<SearchFilter label={t('filters.labels.series')} numSelected={Object.values(seriesState).map(x => x.checked).filter(Boolean).length} open={seriesMenu} onOpenChange={seriesMenuOpened}>
|
||||
<DropdownMenu.Label className="Label">{t('filters.labels.series')}</DropdownMenu.Label>
|
||||
<section>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(weaponSeries.length / 3)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={weaponSeries[i]}
|
||||
onCheckedChange={handleSeriesChange}
|
||||
checked={seriesState[weaponSeries[i]].checked}
|
||||
valueKey={weaponSeries[i]}>
|
||||
{t(`series.${weaponSeries[i]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(weaponSeries.length / 3)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={weaponSeries[i + (weaponSeries.length / 3)]}
|
||||
onCheckedChange={handleSeriesChange}
|
||||
checked={seriesState[weaponSeries[i + (weaponSeries.length / 3)]].checked}
|
||||
valueKey={weaponSeries[i + (weaponSeries.length / 3)]}>
|
||||
{t(`series.${weaponSeries[i + (weaponSeries.length / 3)]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group className="Group">
|
||||
{ Array.from(Array(weaponSeries.length / 3)).map((x, i) => {
|
||||
return (
|
||||
<SearchFilterCheckboxItem
|
||||
key={weaponSeries[i + (2 * (weaponSeries.length / 3))]}
|
||||
onCheckedChange={handleSeriesChange}
|
||||
checked={seriesState[weaponSeries[i + (2 * (weaponSeries.length / 3))]].checked}
|
||||
valueKey={weaponSeries[i + (2 * (weaponSeries.length / 3))]}>
|
||||
{t(`series.${weaponSeries[i + (2 * (weaponSeries.length / 3))]}`)}
|
||||
</SearchFilterCheckboxItem>
|
||||
)}
|
||||
) }
|
||||
</DropdownMenu.Group>
|
||||
</section>
|
||||
</SearchFilter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeaponSearchFilterBar
|
||||
740
package-lock.json
generated
740
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -13,7 +13,7 @@
|
|||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||
"@radix-ui/react-hover-card": "^0.1.5",
|
||||
"@radix-ui/react-label": "^0.1.4",
|
||||
"@radix-ui/react-switch": "^0.1.5",
|
||||
|
|
@ -36,6 +36,8 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-infinite-scroller": "^1.2.5",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-scroll": "^1.8.5",
|
||||
"sass": "^1.49.0",
|
||||
|
|
@ -47,6 +49,7 @@
|
|||
"@types/node": "17.0.11",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-infinite-scroller": "^1.2.2",
|
||||
"@types/react-linkify": "^1.0.1",
|
||||
"@types/react-scroll": "^1.8.3",
|
||||
"eslint": "8.7.0",
|
||||
|
|
|
|||
3
public/icons/Check.svg
Normal file
3
public/icons/Check.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8536 4.85355C11.0488 4.65829 11.0488 4.34171 10.8536 4.14645C10.6583 3.95118 10.3417 3.95118 10.1464 4.14645L5.5 8.79289L3.85355 7.14645C3.65829 6.95118 3.34171 6.95118 3.14645 7.14645C2.95118 7.34171 2.95118 7.65829 3.14645 7.85355L5.14645 9.85355C5.34171 10.0488 5.65829 10.0488 5.85355 9.85355C5.85365 9.85346 5.85375 9.85336 5.85385 9.85326L10.8536 4.85355Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 499 B |
|
|
@ -17,6 +17,18 @@
|
|||
"new": "New",
|
||||
"wiki": "View more on gbf.wiki"
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "Element",
|
||||
"series": "Series",
|
||||
"proficiency": "Proficiency",
|
||||
"rarity": "Rarity"
|
||||
}
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "Null",
|
||||
"wind": "Wind",
|
||||
|
|
@ -36,6 +48,44 @@
|
|||
"light": "Light"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "Sabre",
|
||||
"dagger": "Dagger",
|
||||
"spear": "Spear",
|
||||
"axe": "Axe",
|
||||
"staff": "Staff",
|
||||
"gun": "Gun",
|
||||
"melee": "Melee",
|
||||
"bow": "Bow",
|
||||
"harp": "Harp",
|
||||
"katana": "Katana"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "Seraphic",
|
||||
"grand": "Grand",
|
||||
"opus": "Dark Opus",
|
||||
"draconic": "Draconic",
|
||||
"primal": "Primal",
|
||||
"olden_primal": "Olden Primal",
|
||||
"beast": "Beast",
|
||||
"omega": "Omega",
|
||||
"militis": "Militis",
|
||||
"xeno": "Xeno",
|
||||
"astral": "Astral",
|
||||
"rose": "Rose",
|
||||
"hollowsky": "Hollowsky",
|
||||
"ultima": "Ultima",
|
||||
"bahamut": "Bahamut",
|
||||
"epic": "Epic",
|
||||
"ennead": "Ennead",
|
||||
"cosmos": "Cosmos",
|
||||
"ancestral": "Ancestral",
|
||||
"superlative": "Superlative",
|
||||
"vintage": "Vintage",
|
||||
"class_champion": "Class Champion",
|
||||
"sephira": "Sephira",
|
||||
"new_world": "New World Foundation"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "All time",
|
||||
"last_day": "Last day",
|
||||
|
|
@ -158,10 +208,12 @@
|
|||
"not_found": "You haven't saved any teams"
|
||||
},
|
||||
"search": {
|
||||
"result_count": "{{record_count}} results",
|
||||
"errors": {
|
||||
"start_typing": "Start typing the name of a {{object}}",
|
||||
"min_length": "Type at least 3 characters",
|
||||
"no_results": "No results found for '{{query}}'"
|
||||
"no_results": "No results found for '{{query}}'",
|
||||
"end_results": "No more results"
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "Search for a weapon...",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@
|
|||
"new": "作成",
|
||||
"wiki": "gbf.wikiで詳しく見る"
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "属性",
|
||||
"series": "シリーズ",
|
||||
"proficiency": "武器種",
|
||||
"rarity": "レアリティ"
|
||||
}
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "無",
|
||||
"wind": "風",
|
||||
|
|
@ -36,6 +48,44 @@
|
|||
"light": "光属性"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "剣",
|
||||
"dagger": "短剣",
|
||||
"spear": "槍",
|
||||
"axe": "斧",
|
||||
"staff": "杖",
|
||||
"gun": "銃",
|
||||
"melee": "拳",
|
||||
"bow": "弓",
|
||||
"harp": "琴",
|
||||
"katana": "刀"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "セラフィックウェポン",
|
||||
"grand": "リミテッドシリーズ",
|
||||
"opus": "終末の神器",
|
||||
"draconic": "ドラコニックウェポン",
|
||||
"primal": "プライマルシリーズ",
|
||||
"olden_primal": "オールド・プライマルシリーズ",
|
||||
"beast": "四象武器",
|
||||
"omega": "マグナシリーズ",
|
||||
"militis": "ミーレスシリーズ",
|
||||
"xeno": "六道武器",
|
||||
"astral": "アストラルウェポン",
|
||||
"rose": "ローズシリーズ",
|
||||
"hollowsky": "虚ろなる神器",
|
||||
"ultima": "オメガウェポン",
|
||||
"bahamut": "バハムートウェポン",
|
||||
"epic": "エピックウェポン",
|
||||
"ennead": "エニアドシリーズ",
|
||||
"cosmos": "コスモスシリーズ",
|
||||
"ancestral": "アンセスタルシリーズ",
|
||||
"superlative": "スペリオシリーズ",
|
||||
"vintage": "ヴィンテージシリーズ",
|
||||
"class_champion": "英雄武器",
|
||||
"sephira": "セフィラン・オールドウェポン",
|
||||
"new_world": "新世界の礎"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "全ての期間",
|
||||
"last_day": "1日",
|
||||
|
|
@ -159,10 +209,12 @@
|
|||
"not_found": "編成はまだ保存していません"
|
||||
},
|
||||
"search": {
|
||||
"result_count": "{{record_count}}件",
|
||||
"errors": {
|
||||
"start_typing": "{{object}}名を入力してください",
|
||||
"min_length": "3文字以上を入力してください",
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした"
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||
"end_results": "検索結果これ以上ありません"
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "武器を検索...",
|
||||
|
|
|
|||
|
|
@ -327,6 +327,16 @@ i.tag {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.infinite-scroll-component {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.SearchFilterBar {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
padding: 0 ($unit * 3);
|
||||
}
|
||||
|
||||
@keyframes openModal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
|
|||
4
types/CheckedState.d.ts
vendored
Normal file
4
types/CheckedState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
interface CheckedState {
|
||||
id: number
|
||||
checked: boolean
|
||||
}
|
||||
10
types/ElementState.d.ts
vendored
Normal file
10
types/ElementState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface ElementState {
|
||||
[key: string]: CheckedState
|
||||
null: CheckedState
|
||||
wind: CheckedState
|
||||
fire: CheckedState
|
||||
water: CheckedState
|
||||
earth: CheckedState
|
||||
dark: CheckedState
|
||||
light: CheckedState
|
||||
}
|
||||
13
types/ProficiencyState.d.ts
vendored
Normal file
13
types/ProficiencyState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
interface ProficiencyState {
|
||||
[key: string]: CheckedState
|
||||
sabre: CheckedState
|
||||
dagger: CheckedState
|
||||
spear: CheckedState
|
||||
axe: CheckedState
|
||||
staff: CheckedState
|
||||
melee: CheckedState
|
||||
gun: CheckedState
|
||||
bow: CheckedState
|
||||
harp: CheckedState
|
||||
katana: CheckedState
|
||||
}
|
||||
5
types/RarityState.d.ts
vendored
Normal file
5
types/RarityState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
interface RarityState {
|
||||
[key: string]: CheckedState
|
||||
sr: CheckedState
|
||||
ssr: CheckedState
|
||||
}
|
||||
27
types/WeaponSeries.d.ts
vendored
Normal file
27
types/WeaponSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
interface WeaponSeriesState {
|
||||
[key: string]: CheckedState
|
||||
seraphic: CheckedState
|
||||
grand: CheckedState
|
||||
opus: CheckedState
|
||||
draconic: CheckedState
|
||||
ultima: CheckedState
|
||||
bahamut: CheckedState
|
||||
omega: CheckedState
|
||||
primal: CheckedState
|
||||
olden_primal: CheckedState
|
||||
militis: CheckedState
|
||||
beast: CheckedState
|
||||
rose: CheckedState
|
||||
xeno: CheckedState
|
||||
hollowsky: CheckedState
|
||||
astral: CheckedState
|
||||
epic: CheckedState
|
||||
ennead: CheckedState
|
||||
cosmos: CheckedState
|
||||
ancestral: CheckedState
|
||||
superlative: CheckedState
|
||||
vintage: CheckedState
|
||||
class_champion: CheckedState
|
||||
sephira: CheckedState
|
||||
new_world: CheckedState
|
||||
}
|
||||
|
|
@ -55,12 +55,17 @@ class Api {
|
|||
return axios.post(`${ oauthUrl }/token`, object)
|
||||
}
|
||||
|
||||
search(object: string, query: string, excludes: string, locale: string = 'en') {
|
||||
search({ object, query, filters, locale = "en", page = 0 }:
|
||||
{ object: string, query: string, filters?: { [key: string]: number[] }, locale?: string, page?: number }) {
|
||||
const resourceUrl = `${this.url}/${name}`
|
||||
const url = (excludes.length > 0) ?
|
||||
`${resourceUrl}search/${object}?query=${query}&locale=${locale}&excludes=${excludes}` :
|
||||
`${resourceUrl}search/${object}?query=${query}&locale=${locale}`
|
||||
return axios.get(url)
|
||||
return axios.post(`${resourceUrl}search/${object}`, {
|
||||
search: {
|
||||
query: query,
|
||||
filters: filters,
|
||||
locale: locale,
|
||||
page: page
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
check(resource: string, value: string) {
|
||||
|
|
|
|||
187
utils/emptyStates.tsx
Normal file
187
utils/emptyStates.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
export const emptyRarityState: RarityState = {
|
||||
sr: {
|
||||
id: 2,
|
||||
checked: false
|
||||
},
|
||||
ssr: {
|
||||
id: 3,
|
||||
checked: true
|
||||
}
|
||||
}
|
||||
|
||||
export const emptyElementState: ElementState = {
|
||||
null: {
|
||||
id: 0,
|
||||
checked: false
|
||||
},
|
||||
wind: {
|
||||
id: 1,
|
||||
checked: false
|
||||
},
|
||||
fire: {
|
||||
id: 2,
|
||||
checked: false
|
||||
},
|
||||
water: {
|
||||
id: 3,
|
||||
checked: false
|
||||
},
|
||||
earth: {
|
||||
id: 4,
|
||||
checked: false
|
||||
},
|
||||
dark: {
|
||||
id: 5,
|
||||
checked: false
|
||||
},
|
||||
light: {
|
||||
id: 6,
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
|
||||
export const emptyProficiencyState: ProficiencyState = {
|
||||
sabre: {
|
||||
id: 1,
|
||||
checked: false
|
||||
},
|
||||
dagger: {
|
||||
id: 2,
|
||||
checked: false
|
||||
},
|
||||
axe: {
|
||||
id: 3,
|
||||
checked: false
|
||||
},
|
||||
spear: {
|
||||
id: 4,
|
||||
checked: false
|
||||
},
|
||||
bow: {
|
||||
id: 5,
|
||||
checked: false
|
||||
},
|
||||
staff: {
|
||||
id: 6,
|
||||
checked: false
|
||||
},
|
||||
melee: {
|
||||
id: 7,
|
||||
checked: false
|
||||
},
|
||||
harp: {
|
||||
id: 8,
|
||||
checked: false
|
||||
},
|
||||
gun: {
|
||||
id: 9,
|
||||
checked: false
|
||||
},
|
||||
katana: {
|
||||
id: 10,
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
|
||||
export const emptyWeaponSeriesState: WeaponSeriesState = {
|
||||
seraphic: {
|
||||
id: 0,
|
||||
checked: false
|
||||
},
|
||||
grand: {
|
||||
id: 1,
|
||||
checked: false
|
||||
},
|
||||
opus: {
|
||||
id: 2,
|
||||
checked: false
|
||||
},
|
||||
draconic: {
|
||||
id: 3,
|
||||
checked: false
|
||||
},
|
||||
ultima: {
|
||||
id: 17,
|
||||
checked: false
|
||||
},
|
||||
bahamut: {
|
||||
id: 16,
|
||||
checked: false
|
||||
},
|
||||
regalia: {
|
||||
id: 8,
|
||||
checked: false
|
||||
},
|
||||
omega: {
|
||||
id: 9,
|
||||
checked: false
|
||||
},
|
||||
primal: {
|
||||
id: 6,
|
||||
checked: false
|
||||
},
|
||||
olden_primal: {
|
||||
id: 10,
|
||||
checked: false
|
||||
},
|
||||
militis: {
|
||||
id: 11,
|
||||
checked: false
|
||||
},
|
||||
beast: {
|
||||
id: 7,
|
||||
checked: false
|
||||
},
|
||||
rose: {
|
||||
id: 15,
|
||||
checked: false
|
||||
},
|
||||
xeno: {
|
||||
id: 13,
|
||||
checked: false
|
||||
},
|
||||
hollowsky: {
|
||||
id: 12,
|
||||
checked: false
|
||||
},
|
||||
astral: {
|
||||
id: 14,
|
||||
checked: false
|
||||
},
|
||||
epic: {
|
||||
id: 18,
|
||||
checked: false
|
||||
},
|
||||
ennead: {
|
||||
id: 19,
|
||||
checked: false
|
||||
},
|
||||
cosmos: {
|
||||
id: 20,
|
||||
checked: false
|
||||
},
|
||||
ancestral: {
|
||||
id: 21,
|
||||
checked: false
|
||||
},
|
||||
superlative: {
|
||||
id: 22,
|
||||
checked: false
|
||||
},
|
||||
vintage: {
|
||||
id: 23,
|
||||
checked: false
|
||||
},
|
||||
class_champion: {
|
||||
id: 24,
|
||||
checked: false
|
||||
},
|
||||
sephira: {
|
||||
id: 28,
|
||||
checked: false
|
||||
},
|
||||
new_world: {
|
||||
id: 29,
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
16
utils/stateValues.tsx
Normal file
16
utils/stateValues.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const rarities = ["sr", "ssr"]
|
||||
|
||||
export const elements = ["null", "wind", "fire", "water", "earth", "dark", "light"]
|
||||
|
||||
export const proficiencies = [
|
||||
"sabre", "dagger", "spear", "axe", "staff",
|
||||
"melee", "gun", "bow", "harp", "katana"
|
||||
]
|
||||
|
||||
export const weaponSeries = [
|
||||
"seraphic", "grand", "opus", "draconic", "ultima",
|
||||
"bahamut", "omega", "primal", "olden_primal", "militis",
|
||||
"beast", "rose", "xeno", "hollowsky", "astral",
|
||||
"epic", "ennead", "cosmos", "ancestral", "superlative",
|
||||
"vintage", "class_champion", "sephira", "new_world"
|
||||
]
|
||||
Loading…
Reference in a new issue