Deploy advanced filters (#297)
* Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Added World Series to weapon series empty state (#293) * Push 2023/03 updates to main (#292) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Add World series to empty state * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Enables advanced filters in collections (#289) * Add skeleton of FilterModal * Install react-slider from Radix * Move AccountModal styles to more generic place * Make generic TableField and move styles This is so we have a base for other table rows that use different interactive elements * Implement custom Slider component This inherits from Radix's Slider * Implement SliderTableField * Implemented SwitchTableField * Change enabled switch color * Implement InputTableField * Update modal skeleton * Added localizations for Advanced filters * Update styles for various components Added some new colors and fixed spacing * Added value reporting and fixed a cycle error * Added default values, clearing filters, etc * Default values * Ability to clear filters * Receiving values from components * Fix maximum cycle depth exceeded error * Update TableFields to not error Also optional value is required * Create FilterSet.d.ts * Send filtersets to FilterModal This sends the default filterset and the user's filterset to the filter modal. The default filterset is used when resetting all filters. The users filterset is used so that it is populated with the user's values when they open the modal * Add new localizations * Change types and add default filterset object * Add fast-deep-equal package * Change value in table fields * Input table fields need to be able to be empty * Slider table fields should default to 0 if value isn't provided * Set width of Select in table field in Filter dialog * Add style for filter button with filters active * Swap to using selects for some boolean fields Charge Attack, Full Auto, and Auto Guard are not boolean values since the user can select (and the default should be) to show both on and off values. We swap to using a SelectTableField here to represent this difference. We also added logic for Full Auto and Auto Guard fields since they are tied together in some cases (you can't show Auto Guard teams that have Full Auto disabled) * Populate values from defaultFilterSet * Update how we save and propagate filters We save filterset in a local state, because the FilterBar will send it down to us from cookies. We then set each individual property from that filter set. We set inputs to have a placeholder, as max buttons and max turns could not be set (null). Then, we only send those fields when they have a value provided by the user. * Remove default filterset This was moved to a utils/ file * Propagate filters from modal This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active. * Implement advanced filters on Teams page * Add skeleton of FilterModal * Make generic TableField and move styles This is so we have a base for other table rows that use different interactive elements * Implement custom Slider component This inherits from Radix's Slider * Implement SliderTableField * Implemented SwitchTableField * Implement InputTableField * Update modal skeleton * Added localizations for Advanced filters * Update styles for various components Added some new colors and fixed spacing * Added value reporting and fixed a cycle error * Added default values, clearing filters, etc * Default values * Ability to clear filters * Receiving values from components * Fix maximum cycle depth exceeded error * Update TableFields to not error Also optional value is required * Create FilterSet.d.ts * Send filtersets to FilterModal This sends the default filterset and the user's filterset to the filter modal. The default filterset is used when resetting all filters. The users filterset is used so that it is populated with the user's values when they open the modal * Add new localizations * Change types and add default filterset object * Change value in table fields * Input table fields need to be able to be empty * Slider table fields should default to 0 if value isn't provided * Set width of Select in table field in Filter dialog * Add style for filter button with filters active * Swap to using selects for some boolean fields Charge Attack, Full Auto, and Auto Guard are not boolean values since the user can select (and the default should be) to show both on and off values. We swap to using a SelectTableField here to represent this difference. We also added logic for Full Auto and Auto Guard fields since they are tied together in some cases (you can't show Auto Guard teams that have Full Auto disabled) * Populate values from defaultFilterSet * Update how we save and propagate filters We save filterset in a local state, because the FilterBar will send it down to us from cookies. We then set each individual property from that filter set. We set inputs to have a placeholder, as max buttons and max turns could not be set (null). Then, we only send those fields when they have a value provided by the user. * Remove default filterset This was moved to a utils/ file * Propagate filters from modal This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active. * GridRep adjustments * Properly unset mainhand when cells get reused and the new team doesnt have one * Slightly better styling to make the grid more correct * Fix bad merge * Add advanced filter support to saved and profile pages * Fix auto guard text * Ensure fetchTeams callback is updated with filters * Add auto guard icon to GridRep * Disable max buttons and turns * Fix build errors
This commit is contained in:
parent
7b54791bb3
commit
968ae5c41e
35 changed files with 1475 additions and 328 deletions
|
|
@ -5,17 +5,6 @@
|
|||
width: $unit * 64;
|
||||
overflow-y: hidden;
|
||||
|
||||
.Fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
gap: $unit-4x;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,11 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.Spaced {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +185,17 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.Fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
gap: $unit-4x;
|
||||
}
|
||||
}
|
||||
|
||||
&.Conflict {
|
||||
$weapon-diameter: 14rem;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,24 @@
|
|||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Button.Filter.Blended {
|
||||
&.FiltersActive .Accessory svg {
|
||||
fill: var(--accent-blue);
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
|
||||
.Accessory svg {
|
||||
fill: none;
|
||||
stroke: var(--button-text);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
import equals from 'fast-deep-equal'
|
||||
|
||||
import FilterModal from '~components/FilterModal'
|
||||
import RaidDropdown from '~components/RaidDropdown'
|
||||
|
||||
import './index.scss'
|
||||
import Select from '~components/Select'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
import Button from '~components/Button'
|
||||
|
||||
import { defaultFilterset } from '~utils/defaultFilters'
|
||||
|
||||
import FilterIcon from '~public/icons/Filter.svg'
|
||||
|
||||
import './index.scss'
|
||||
import { getCookie } from 'cookies-next'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
|
|
@ -14,15 +22,7 @@ interface Props {
|
|||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
onFilter: ({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) => void
|
||||
onFilter: (filters: FilterSet) => void
|
||||
}
|
||||
|
||||
const FilterBar = (props: Props) => {
|
||||
|
|
@ -32,12 +32,32 @@ const FilterBar = (props: Props) => {
|
|||
const [recencyOpen, setRecencyOpen] = useState(false)
|
||||
const [elementOpen, setElementOpen] = useState(false)
|
||||
|
||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||
const [advancedFilters, setAdvancedFilters] = useState<FilterSet>({})
|
||||
|
||||
const [matchesDefaultFilters, setMatchesDefaultFilters] = useState(false)
|
||||
// Set up classes object for showing shadow on scroll
|
||||
const classes = classNames({
|
||||
FilterBar: true,
|
||||
shadow: props.scrolled,
|
||||
})
|
||||
|
||||
const filterButtonClasses = classNames({
|
||||
Filter: true,
|
||||
FiltersActive: !matchesDefaultFilters,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch user's advanced filters
|
||||
const filtersCookie = getCookie('filters')
|
||||
if (filtersCookie) setAdvancedFilters(JSON.parse(filtersCookie as string))
|
||||
else setAdvancedFilters(defaultFilterset)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setMatchesDefaultFilters(equals(advancedFilters, defaultFilterset))
|
||||
}, [advancedFilters, defaultFilterset])
|
||||
|
||||
function openElementSelect() {
|
||||
setElementOpen(!elementOpen)
|
||||
}
|
||||
|
|
@ -48,16 +68,21 @@ const FilterBar = (props: Props) => {
|
|||
|
||||
function elementSelectChanged(value: string) {
|
||||
const elementValue = parseInt(value)
|
||||
props.onFilter({ element: elementValue })
|
||||
props.onFilter({ element: elementValue, ...advancedFilters })
|
||||
}
|
||||
|
||||
function recencySelectChanged(value: string) {
|
||||
const recencyValue = parseInt(value)
|
||||
props.onFilter({ recency: recencyValue })
|
||||
props.onFilter({ recency: recencyValue, ...advancedFilters })
|
||||
}
|
||||
|
||||
function raidSelectChanged(slug?: string) {
|
||||
props.onFilter({ raidSlug: slug })
|
||||
props.onFilter({ raidSlug: slug, ...advancedFilters })
|
||||
}
|
||||
|
||||
function handleAdvancedFiltersChanged(filters: FilterSet) {
|
||||
setAdvancedFilters(filters)
|
||||
props.onFilter(filters)
|
||||
}
|
||||
|
||||
function onSelectChange(name: 'element' | 'recency') {
|
||||
|
|
@ -66,6 +91,7 @@ const FilterBar = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes}>
|
||||
{props.children}
|
||||
<div className="Filters">
|
||||
|
|
@ -139,8 +165,23 @@ const FilterBar = (props: Props) => {
|
|||
{t('recency.last_year')}
|
||||
</SelectItem>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
className={filterButtonClasses}
|
||||
blended={true}
|
||||
leftAccessoryIcon={<FilterIcon />}
|
||||
onClick={() => setFilterModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FilterModal
|
||||
defaultFilterSet={defaultFilterset}
|
||||
filterSet={advancedFilters}
|
||||
open={filterModalOpen}
|
||||
onOpenChange={setFilterModalOpen}
|
||||
sendAdvancedFilters={handleAdvancedFiltersChanged}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
15
components/FilterModal/index.scss
Normal file
15
components/FilterModal/index.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.Dialog {
|
||||
.Filter.DialogContent {
|
||||
overflow: hidden;
|
||||
|
||||
.TableField .Right .SelectTrigger.Table {
|
||||
width: $unit-20x;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogFooter .Buttons .Button.Blended {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
435
components/FilterModal/index.tsx
Normal file
435
components/FilterModal/index.tsx
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { getCookie, setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
} from '~components/Dialog'
|
||||
import DialogContent from '~components/DialogContent'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import InputTableField from '~components/InputTableField'
|
||||
import SelectTableField from '~components/SelectTableField'
|
||||
import SliderTableField from '~components/SliderTableField'
|
||||
import SwitchTableField from '~components/SwitchTableField'
|
||||
import SelectItem from '~components/SelectItem'
|
||||
|
||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props extends DialogProps {
|
||||
defaultFilterSet: FilterSet
|
||||
filterSet: FilterSet
|
||||
sendAdvancedFilters: (filters: FilterSet) => void
|
||||
}
|
||||
|
||||
const MAX_CHARACTERS = 5
|
||||
const MAX_WEAPONS = 13
|
||||
const MAX_SUMMONS = 8
|
||||
|
||||
const FilterModal = (props: Props) => {
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const locale = router.locale
|
||||
|
||||
// Set up translation
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Refs
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
const footerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
||||
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
||||
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
||||
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
||||
|
||||
// Filter states
|
||||
const [fullAuto, setFullAuto] = useState(props.defaultFilterSet.full_auto)
|
||||
const [autoGuard, setAutoGuard] = useState(props.defaultFilterSet.auto_guard)
|
||||
const [chargeAttack, setChargeAttack] = useState(
|
||||
props.defaultFilterSet.charge_attack
|
||||
)
|
||||
const [minCharacterCount, setMinCharacterCount] = useState(
|
||||
props.defaultFilterSet.characters_count
|
||||
)
|
||||
const [minWeaponCount, setMinWeaponCount] = useState(
|
||||
props.defaultFilterSet.weapons_count
|
||||
)
|
||||
const [minSummonCount, setMinSummonCount] = useState(
|
||||
props.defaultFilterSet.summons_count
|
||||
)
|
||||
const [maxButtonsCount, setMaxButtonsCount] = useState(
|
||||
props.defaultFilterSet.button_count
|
||||
)
|
||||
const [maxTurnsCount, setMaxTurnsCount] = useState(
|
||||
props.defaultFilterSet.turn_count
|
||||
)
|
||||
const [userQuality, setUserQuality] = useState(
|
||||
props.defaultFilterSet.user_quality
|
||||
)
|
||||
const [nameQuality, setNameQuality] = useState(
|
||||
props.defaultFilterSet.name_quality
|
||||
)
|
||||
const [originalOnly, setOriginalOnly] = useState(
|
||||
props.defaultFilterSet.original
|
||||
)
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
if (props.open !== undefined) setOpen(props.open)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setFilterSet(props.filterSet)
|
||||
}, [props.filterSet])
|
||||
|
||||
useEffect(() => {
|
||||
setFullAuto(filterSet.full_auto)
|
||||
setAutoGuard(filterSet.auto_guard)
|
||||
setChargeAttack(filterSet.charge_attack)
|
||||
|
||||
setMinCharacterCount(filterSet.characters_count)
|
||||
setMinSummonCount(filterSet.summons_count)
|
||||
setMinWeaponCount(filterSet.weapons_count)
|
||||
|
||||
setMaxButtonsCount(filterSet.button_count)
|
||||
setMaxTurnsCount(filterSet.turn_count)
|
||||
|
||||
setNameQuality(filterSet.name_quality)
|
||||
setUserQuality(filterSet.user_quality)
|
||||
setOriginalOnly(filterSet.original)
|
||||
}, [filterSet])
|
||||
|
||||
function openSelect(name: 'charge_attack' | 'full_auto' | 'auto_guard') {
|
||||
setChargeAttackOpen(name === 'charge_attack' ? !chargeAttackOpen : false)
|
||||
setFullAutoOpen(name === 'full_auto' ? !fullAutoOpen : false)
|
||||
setAutoGuardOpen(name === 'auto_guard' ? !autoGuardOpen : false)
|
||||
}
|
||||
|
||||
function saveFilters() {
|
||||
const filters: FilterSet = {}
|
||||
filters.full_auto = fullAuto
|
||||
filters.auto_guard = autoGuard
|
||||
filters.charge_attack = chargeAttack
|
||||
filters.characters_count = minCharacterCount
|
||||
filters.weapons_count = minWeaponCount
|
||||
filters.summons_count = minSummonCount
|
||||
filters.name_quality = nameQuality
|
||||
filters.user_quality = userQuality
|
||||
filters.original = originalOnly
|
||||
|
||||
if (maxButtonsCount) filters.button_count = maxButtonsCount
|
||||
if (maxTurnsCount) filters.turn_count = maxTurnsCount
|
||||
|
||||
setCookie('filters', filters, { path: '/' })
|
||||
props.sendAdvancedFilters(filters)
|
||||
openChange()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
setFullAuto(props.defaultFilterSet.full_auto)
|
||||
setAutoGuard(props.defaultFilterSet.auto_guard)
|
||||
setChargeAttack(props.defaultFilterSet.charge_attack)
|
||||
setMinCharacterCount(props.defaultFilterSet.characters_count)
|
||||
setMinWeaponCount(props.defaultFilterSet.weapons_count)
|
||||
setMinSummonCount(props.defaultFilterSet.summons_count)
|
||||
setMaxButtonsCount(props.defaultFilterSet.button_count)
|
||||
setMaxTurnsCount(props.defaultFilterSet.turn_count)
|
||||
setUserQuality(props.defaultFilterSet.user_quality)
|
||||
setNameQuality(props.defaultFilterSet.name_quality)
|
||||
setOriginalOnly(props.defaultFilterSet.original)
|
||||
}
|
||||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setOpen(false)
|
||||
if (props.onOpenChange) props.onOpenChange(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
if (props.onOpenChange) props.onOpenChange(true)
|
||||
}
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||||
event.preventDefault()
|
||||
openChange()
|
||||
}
|
||||
|
||||
function onOpenAutoFocus(event: Event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Value listeners
|
||||
function handleChargeAttackValueChange(value: string) {
|
||||
setChargeAttack(parseInt(value))
|
||||
}
|
||||
|
||||
function handleFullAutoValueChange(value: string) {
|
||||
const newValue = parseInt(value)
|
||||
setFullAuto(newValue)
|
||||
if (newValue === 0 || (newValue === -1 && autoGuard === 1))
|
||||
setAutoGuard(newValue)
|
||||
}
|
||||
|
||||
function handleAutoGuardValueChange(value: string) {
|
||||
const newValue = parseInt(value)
|
||||
setAutoGuard(newValue)
|
||||
if (newValue === 1 || (newValue === -1 && fullAuto === 0))
|
||||
setFullAuto(newValue)
|
||||
}
|
||||
|
||||
function handleMinCharactersValueChange(value: number) {
|
||||
setMinCharacterCount(value)
|
||||
}
|
||||
|
||||
function handleMinSummonsValueChange(value: number) {
|
||||
setMinSummonCount(value)
|
||||
}
|
||||
|
||||
function handleMinWeaponsValueChange(value: number) {
|
||||
setMinWeaponCount(value)
|
||||
}
|
||||
|
||||
function handleMaxButtonsCountValueChange(value: number) {
|
||||
setMaxButtonsCount(value)
|
||||
}
|
||||
|
||||
function handleMaxTurnsCountValueChange(value: number) {
|
||||
setMaxTurnsCount(value)
|
||||
}
|
||||
|
||||
function handleNameQualityValueChange(value: boolean) {
|
||||
setNameQuality(value)
|
||||
}
|
||||
|
||||
function handleUserQualityValueChange(value: boolean) {
|
||||
setUserQuality(value)
|
||||
}
|
||||
|
||||
function handleOriginalOnlyValueChange(value: boolean) {
|
||||
setOriginalOnly(value)
|
||||
}
|
||||
|
||||
// Sliders
|
||||
const minCharactersField = () => (
|
||||
<SliderTableField
|
||||
name="min_characters"
|
||||
description={t('modals.filters.descriptions.min_characters')}
|
||||
label={t('modals.filters.labels.min_characters')}
|
||||
value={minCharacterCount}
|
||||
min={0}
|
||||
max={MAX_CHARACTERS}
|
||||
step={1}
|
||||
onValueChange={handleMinCharactersValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const minWeaponsField = () => (
|
||||
<SliderTableField
|
||||
name="min_weapons"
|
||||
description={t('modals.filters.descriptions.min_weapons')}
|
||||
label={t('modals.filters.labels.min_weapons')}
|
||||
value={minWeaponCount}
|
||||
min={0}
|
||||
max={MAX_WEAPONS}
|
||||
step={1}
|
||||
onValueChange={handleMinWeaponsValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const minSummonsField = () => (
|
||||
<SliderTableField
|
||||
name="min_summons"
|
||||
description={t('modals.filters.descriptions.min_summons')}
|
||||
label={t('modals.filters.labels.min_summons')}
|
||||
value={minSummonCount}
|
||||
min={0}
|
||||
max={MAX_SUMMONS}
|
||||
step={1}
|
||||
onValueChange={handleMinSummonsValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// Selects
|
||||
const fullAutoField = () => (
|
||||
<SelectTableField
|
||||
name="full_auto"
|
||||
label={t('modals.filters.labels.full_auto')}
|
||||
open={fullAutoOpen}
|
||||
value={`${fullAuto}`}
|
||||
onOpenChange={() => openSelect('full_auto')}
|
||||
onClose={() => setFullAutoOpen(false)}
|
||||
onChange={handleFullAutoValueChange}
|
||||
>
|
||||
<SelectItem key="on-off" value="-1">
|
||||
{t('modals.filters.options.on_and_off')}
|
||||
</SelectItem>
|
||||
<SelectItem key="on" value="1">
|
||||
{t('modals.filters.options.on')}
|
||||
</SelectItem>
|
||||
<SelectItem key="off" value="0">
|
||||
{t('modals.filters.options.off')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const autoGuardField = () => (
|
||||
<SelectTableField
|
||||
name="auto_guard"
|
||||
label={t('modals.filters.labels.auto_guard')}
|
||||
open={autoGuardOpen}
|
||||
value={`${autoGuard}`}
|
||||
onOpenChange={() => openSelect('auto_guard')}
|
||||
onClose={() => setAutoGuardOpen(false)}
|
||||
onChange={handleAutoGuardValueChange}
|
||||
>
|
||||
<SelectItem key="on-off" value="-1">
|
||||
{t('modals.filters.options.on_and_off')}
|
||||
</SelectItem>
|
||||
<SelectItem key="on" value="1">
|
||||
{t('modals.filters.options.on')}
|
||||
</SelectItem>
|
||||
<SelectItem key="off" value="0">
|
||||
{t('modals.filters.options.off')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const chargeAttackField = () => (
|
||||
<SelectTableField
|
||||
name="charge_attack"
|
||||
label={t('modals.filters.labels.charge_attack')}
|
||||
open={chargeAttackOpen}
|
||||
value={`${chargeAttack}`}
|
||||
onOpenChange={() => openSelect('charge_attack')}
|
||||
onClose={() => setChargeAttackOpen(false)}
|
||||
onChange={handleChargeAttackValueChange}
|
||||
>
|
||||
<SelectItem key="on-off" value="-1">
|
||||
{t('modals.filters.options.on_and_off')}
|
||||
</SelectItem>
|
||||
<SelectItem key="on" value="1">
|
||||
{t('modals.filters.options.on')}
|
||||
</SelectItem>
|
||||
<SelectItem key="off" value="0">
|
||||
{t('modals.filters.options.off')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
// Switches
|
||||
const nameQualityField = () => (
|
||||
<SwitchTableField
|
||||
name="name_quality"
|
||||
label={t('modals.filters.labels.name_quality')}
|
||||
value={nameQuality}
|
||||
onValueChange={handleNameQualityValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const userQualityField = () => (
|
||||
<SwitchTableField
|
||||
name="user_quality"
|
||||
label={t('modals.filters.labels.user_quality')}
|
||||
value={userQuality}
|
||||
onValueChange={handleUserQualityValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const originalOnlyField = () => (
|
||||
<SwitchTableField
|
||||
name="original_only"
|
||||
label={t('modals.filters.labels.original_only')}
|
||||
value={originalOnly}
|
||||
onValueChange={handleOriginalOnlyValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// Inputs
|
||||
const maxButtonsField = () => (
|
||||
<InputTableField
|
||||
name="min_characters"
|
||||
description={t('modals.filters.descriptions.max_buttons')}
|
||||
placeholder="0"
|
||||
label={t('modals.filters.labels.max_buttons')}
|
||||
value={maxButtonsCount}
|
||||
onValueChange={handleMaxButtonsCountValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const maxTurnsField = () => (
|
||||
<InputTableField
|
||||
name="min_turns"
|
||||
description={t('modals.filters.descriptions.max_turns')}
|
||||
placeholder="0"
|
||||
label={t('modals.filters.labels.max_turns')}
|
||||
value={maxTurnsCount}
|
||||
onValueChange={handleMaxTurnsCountValueChange}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Filter"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
onOpenAutoFocus={onOpenAutoFocus}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<div className="DialogTop">
|
||||
<DialogTitle className="DialogTitle">
|
||||
{t('modals.filters.title')}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="Fields">
|
||||
{chargeAttackField()}
|
||||
{fullAutoField()}
|
||||
{autoGuardField()}
|
||||
{/* {maxButtonsField()} */}
|
||||
{/* {maxTurnsField()} */}
|
||||
{minCharactersField()}
|
||||
{minSummonsField()}
|
||||
{minWeaponsField()}
|
||||
{nameQualityField()}
|
||||
{userQualityField()}
|
||||
{originalOnlyField()}
|
||||
</div>
|
||||
<div className="DialogFooter" ref={footerRef}>
|
||||
<div className="Buttons Spaced">
|
||||
<Button
|
||||
blended={true}
|
||||
text={t('modals.filters.buttons.clear')}
|
||||
onClick={resetFilters}
|
||||
/>
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.filters.buttons.confirm')}
|
||||
onClick={saveFilters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterModal
|
||||
|
|
@ -35,9 +35,9 @@
|
|||
}
|
||||
|
||||
& > .Grid {
|
||||
aspect-ratio: 2/1;
|
||||
aspect-ratio: 2/0.95;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr; /* left column takes up 1 fraction, right column takes up 3 fractions */
|
||||
grid-template-columns: 1fr 3.36fr; /* left column takes up 1 fraction, right column takes up 3 fractions */
|
||||
grid-gap: $unit; /* add a gap of 8px between grid items */
|
||||
|
||||
.Weapon {
|
||||
|
|
@ -49,6 +49,7 @@
|
|||
aspect-ratio: 73/153;
|
||||
display: grid;
|
||||
grid-column: 1 / 2; /* spans one column */
|
||||
max-height: 140px;
|
||||
}
|
||||
|
||||
.GridWeapons {
|
||||
|
|
@ -62,6 +63,8 @@
|
|||
1fr
|
||||
); /* create 3 rows, each taking up 1 fraction */
|
||||
gap: $unit;
|
||||
// column-gap: $unit;
|
||||
// row-gap: $unit-2x;
|
||||
}
|
||||
|
||||
.Grid.Weapon {
|
||||
|
|
@ -136,9 +139,25 @@
|
|||
}
|
||||
|
||||
.Properties {
|
||||
.auto {
|
||||
display: inline-flex;
|
||||
gap: $unit-half;
|
||||
flex-direction: row;
|
||||
margin-left: $unit-half;
|
||||
}
|
||||
|
||||
.full_auto {
|
||||
color: var(--full-auto-label-text);
|
||||
}
|
||||
|
||||
.auto_guard {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
svg {
|
||||
fill: var(--full-auto-label-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.raid {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { formatTimeAgo } from '~utils/timeAgo'
|
|||
import Button from '~components/Button'
|
||||
|
||||
import SaveIcon from '~public/icons/Save.svg'
|
||||
|
||||
import ShieldIcon from '~public/icons/Shield.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -23,6 +23,7 @@ interface Props {
|
|||
grid: GridWeapon[]
|
||||
user?: User
|
||||
fullAuto: boolean
|
||||
autoGuard: boolean
|
||||
favorited: boolean
|
||||
createdAt: Date
|
||||
displayUser?: boolean | false
|
||||
|
|
@ -62,14 +63,21 @@ const GridRep = (props: Props) => {
|
|||
const newWeapons = Array(numWeapons)
|
||||
const gridWeapons = Array(numWeapons)
|
||||
|
||||
let foundMainhand = false
|
||||
for (const [key, value] of Object.entries(props.grid)) {
|
||||
if (value.position == -1) setMainhand(value.object)
|
||||
else if (!value.mainhand && value.position != null) {
|
||||
if (value.position == -1) {
|
||||
setMainhand(value.object)
|
||||
foundMainhand = true
|
||||
} else if (!value.mainhand && value.position != null) {
|
||||
newWeapons[value.position] = value.object
|
||||
gridWeapons[value.position] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMainhand) {
|
||||
setMainhand(undefined)
|
||||
}
|
||||
|
||||
setWeapons(newWeapons)
|
||||
setGrid(gridWeapons)
|
||||
}, [props.grid])
|
||||
|
|
@ -164,6 +172,26 @@ const GridRep = (props: Props) => {
|
|||
</div>
|
||||
)
|
||||
|
||||
function fullAutoString() {
|
||||
const fullAutoElement = (
|
||||
<span className="full_auto">
|
||||
{` · ${t('party.details.labels.full_auto')}`}
|
||||
</span>
|
||||
)
|
||||
|
||||
const autoGuardElement = (
|
||||
<span className="auto_guard">
|
||||
<ShieldIcon />
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="auto">
|
||||
{fullAutoElement}
|
||||
{props.autoGuard ? autoGuardElement : ''}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const details = (
|
||||
<div className="Details">
|
||||
<h2 className={titleClass}>{props.name ? props.name : t('no_title')}</h2>
|
||||
|
|
@ -172,13 +200,7 @@ const GridRep = (props: Props) => {
|
|||
<span className={raidClass}>
|
||||
{props.raid ? props.raid.name[locale] : t('no_raid')}
|
||||
</span>
|
||||
{props.fullAuto ? (
|
||||
<span className="full_auto">
|
||||
{` · ${t('party.details.labels.full_auto')}`}
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{props.fullAuto ? fullAutoString() : ''}
|
||||
</div>
|
||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
|
||||
{formatTimeAgo(props.createdAt, locale)}
|
||||
|
|
|
|||
4
components/InputTableField/index.scss
Normal file
4
components/InputTableField/index.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.InputField.TableField .Input {
|
||||
text-align: right;
|
||||
width: $unit-8x;
|
||||
}
|
||||
56
components/InputTableField/index.tsx
Normal file
56
components/InputTableField/index.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Input from '~components/Input'
|
||||
import TableField from '~components/TableField'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
value?: number
|
||||
className?: string
|
||||
imageAlt?: string
|
||||
imageClass?: string
|
||||
imageSrc?: string[]
|
||||
onValueChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputTableField = (props: Props) => {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value) setValue(props.value)
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
props.onValueChange(value)
|
||||
}, [value])
|
||||
|
||||
function onInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setValue(parseInt(event.currentTarget?.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<TableField
|
||||
name={props.name}
|
||||
className="InputField"
|
||||
imageAlt={props.imageAlt}
|
||||
imageClass={props.imageClass}
|
||||
imageSrc={props.imageSrc}
|
||||
label={props.label}
|
||||
>
|
||||
<Input
|
||||
className="Bound"
|
||||
placeholder={props.placeholder}
|
||||
type="number"
|
||||
value={value ? `${value}` : ''}
|
||||
step={1}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</TableField>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputTableField
|
||||
|
|
@ -480,6 +480,7 @@ const PartyDetails = (props: Props) => {
|
|||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
fullAuto={party.full_auto}
|
||||
autoGuard={party.auto_guard}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
|
|
@ -731,7 +732,7 @@ const PartyDetails = (props: Props) => {
|
|||
})}
|
||||
>
|
||||
{`${t('party.details.labels.auto_guard')} ${
|
||||
fullAuto ? 'On' : 'Off'
|
||||
autoGuard ? 'On' : 'Off'
|
||||
}`}
|
||||
</Token>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
.TableField {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: $unit-2x;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
width: 100%;
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.Image {
|
||||
display: none;
|
||||
|
||||
.preview {
|
||||
$diameter: $unit-5x;
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
|
||||
img {
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-small;
|
||||
line-height: 1.1;
|
||||
max-width: 300px;
|
||||
|
||||
&.jp {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit-2x;
|
||||
width: 100%;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
.Image {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTrigger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
$diameter: $unit * 6;
|
||||
background-color: $grey-90;
|
||||
border-radius: 999px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
img {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-20;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-20;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-20;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-20;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Select from '~components/Select'
|
||||
import TableField from '~components/TableField'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
|
|
@ -27,32 +28,14 @@ const SelectTableField = (props: Props) => {
|
|||
if (props.value) setValue(props.value)
|
||||
}, [props.value])
|
||||
|
||||
const image = () => {
|
||||
return props.imageSrc && props.imageSrc.length > 0 ? (
|
||||
<div className={`preview ${props.imageClass}`}>
|
||||
<img
|
||||
alt={props.imageAlt}
|
||||
srcSet={props.imageSrc.join(', ')}
|
||||
src={props.imageSrc[0]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames({ TableField: true }, props.className)}>
|
||||
<div className="Left">
|
||||
<div className="Info">
|
||||
<h3>{props.label}</h3>
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
<div className="Image">{image()}</div>
|
||||
</div>
|
||||
|
||||
<div className="Right">
|
||||
<div className="Image">{image()}</div>
|
||||
<TableField
|
||||
name={props.name}
|
||||
imageAlt={props.imageAlt}
|
||||
imageClass={props.imageClass}
|
||||
imageSrc={props.imageSrc}
|
||||
label={props.label}
|
||||
>
|
||||
<Select
|
||||
name={props.name}
|
||||
open={props.open}
|
||||
|
|
@ -65,8 +48,7 @@ const SelectTableField = (props: Props) => {
|
|||
>
|
||||
{props.children}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</TableField>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
48
components/Slider/index.scss
Normal file
48
components/Slider/index.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.Slider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
width: $unit-20x;
|
||||
height: 20px;
|
||||
|
||||
.SliderTrack {
|
||||
background-color: var(--button-bg);
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
border-radius: 9999px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.SliderRange {
|
||||
position: absolute;
|
||||
background-color: var(--accent-blue);
|
||||
border-radius: 9999px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.SliderThumb {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--slider-thumb-bg);
|
||||
box-shadow: 0 2px 10px var(--slider-thumb-shadow);
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slider-thumb-bg-hover);
|
||||
box-shadow: 0 2px 10px var(--slider-thumb-shadow-hover);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 5px rgba($accent--blue--light, 0.6);
|
||||
}
|
||||
}
|
||||
32
components/Slider/index.tsx
Normal file
32
components/Slider/index.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import type { SliderProps } from '@radix-ui/react-slider'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Slider = React.forwardRef<HTMLDivElement, Props & SliderProps>(
|
||||
(props, forwardedRef) => {
|
||||
const value = props.value || props.defaultValue
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Slider
|
||||
{...props}
|
||||
className={classNames({ Slider: true }, props.className)}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SliderPrimitive.Track className="SliderTrack">
|
||||
<SliderPrimitive.Range className="SliderRange" />
|
||||
</SliderPrimitive.Track>
|
||||
|
||||
<SliderPrimitive.Thumb className="SliderThumb" />
|
||||
</SliderPrimitive.Slider>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Slider.displayName = 'Slider'
|
||||
|
||||
export default Slider
|
||||
8
components/SliderTableField/index.scss
Normal file
8
components/SliderTableField/index.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.SliderField.TableField {
|
||||
min-height: $unit-4x;
|
||||
|
||||
.Input {
|
||||
text-align: right;
|
||||
width: $unit-8x;
|
||||
}
|
||||
}
|
||||
78
components/SliderTableField/index.tsx
Normal file
78
components/SliderTableField/index.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Input from '~components/Input'
|
||||
import Slider from '~components/Slider'
|
||||
import TableField from '~components/TableField'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
label: string
|
||||
description?: string
|
||||
value?: number
|
||||
className?: string
|
||||
imageAlt?: string
|
||||
imageClass?: string
|
||||
imageSrc?: string[]
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
onValueChange: (value: number) => void
|
||||
}
|
||||
|
||||
const SliderTableField = (props: Props) => {
|
||||
const [value, setValue] = useState(props.value)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value !== undefined && props.value !== value)
|
||||
setValue(props.value)
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined && value !== props.value) props.onValueChange(value)
|
||||
}, [value])
|
||||
|
||||
function handleValueCommit(value: number[]) {
|
||||
setValue(value[0])
|
||||
}
|
||||
|
||||
function handleValueChange(value: number[]) {
|
||||
setValue(value[0])
|
||||
}
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setValue(parseInt(event.currentTarget?.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<TableField
|
||||
name={props.name}
|
||||
className="SliderField"
|
||||
imageAlt={props.imageAlt}
|
||||
imageClass={props.imageClass}
|
||||
imageSrc={props.imageSrc}
|
||||
label={props.label}
|
||||
>
|
||||
<Slider
|
||||
name={props.name}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
value={[value ? value : 0]}
|
||||
onValueChange={handleValueChange}
|
||||
onValueCommit={handleValueCommit}
|
||||
/>
|
||||
<Input
|
||||
className="Bound"
|
||||
type="number"
|
||||
value={`${value}`}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</TableField>
|
||||
)
|
||||
}
|
||||
|
||||
export default SliderTableField
|
||||
|
|
@ -9,12 +9,14 @@
|
|||
width: 58px;
|
||||
height: $height;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $grey-15;
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--accent-blue-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: $grey-15;
|
||||
background: var(--accent-blue);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
|
|
|||
0
components/SwitchTableField/index.scss
Normal file
0
components/SwitchTableField/index.scss
Normal file
52
components/SwitchTableField/index.tsx
Normal file
52
components/SwitchTableField/index.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Switch from '~components/Switch'
|
||||
import TableField from '~components/TableField'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
label: string
|
||||
description?: string
|
||||
value?: boolean
|
||||
className?: string
|
||||
imageAlt?: string
|
||||
imageClass?: string
|
||||
imageSrc?: string[]
|
||||
onValueChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const SwitchTableField = (props: Props) => {
|
||||
const [value, setValue] = useState(props.value)
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== props.value) setValue(props.value)
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined) props.onValueChange(value)
|
||||
}, [value])
|
||||
|
||||
function onValueChange(value: boolean) {
|
||||
setValue(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableField
|
||||
name={props.name}
|
||||
className="SwitchField"
|
||||
imageAlt={props.imageAlt}
|
||||
imageClass={props.imageClass}
|
||||
imageSrc={props.imageSrc}
|
||||
label={props.label}
|
||||
>
|
||||
<Switch
|
||||
name={props.name}
|
||||
checked={value}
|
||||
onCheckedChange={onValueChange}
|
||||
/>
|
||||
</TableField>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwitchTableField
|
||||
124
components/TableField/index.scss
Normal file
124
components/TableField/index.scss
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
.TableField {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: $unit-2x;
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-content: space-between;
|
||||
padding: $unit-half 0;
|
||||
width: 100%;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&:hover .Left .Info h3 {
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.Left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
width: 100%;
|
||||
|
||||
.Info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.Image {
|
||||
display: none;
|
||||
|
||||
.preview {
|
||||
$diameter: $unit-5x;
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
|
||||
img {
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--text-tertiary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-small;
|
||||
line-height: 1.1;
|
||||
max-width: 300px;
|
||||
|
||||
&.jp {
|
||||
max-width: 270px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Right {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit-2x;
|
||||
width: 100%;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
.Image {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTrigger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
$diameter: $unit * 6;
|
||||
background-color: $grey-90;
|
||||
border-radius: 999px;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
|
||||
img {
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
background: $fire-bg-20;
|
||||
}
|
||||
|
||||
&.water {
|
||||
background: $water-bg-20;
|
||||
}
|
||||
|
||||
&.wind {
|
||||
background: $wind-bg-20;
|
||||
}
|
||||
|
||||
&.earth {
|
||||
background: $earth-bg-20;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: $dark-bg-10;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background: $light-bg-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
components/TableField/index.tsx
Normal file
48
components/TableField/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import classNames from 'classnames'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
label: string
|
||||
description?: string
|
||||
className?: string
|
||||
imageAlt?: string
|
||||
imageClass?: string
|
||||
imageSrc?: string[]
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const TableField = (props: Props) => {
|
||||
const image = () => {
|
||||
return props.imageSrc && props.imageSrc.length > 0 ? (
|
||||
<div className={`preview ${props.imageClass}`}>
|
||||
<img
|
||||
alt={props.imageAlt}
|
||||
srcSet={props.imageSrc.join(', ')}
|
||||
src={props.imageSrc[0]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames({ TableField: true }, props.className)}>
|
||||
<div className="Left">
|
||||
<div className="Info">
|
||||
<h3>{props.label}</h3>
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
<div className="Image">{image()}</div>
|
||||
</div>
|
||||
|
||||
<div className="Right">
|
||||
<div className="Image">{image()}</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableField
|
||||
102
package-lock.json
generated
102
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fix-date": "^1.1.6",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
|
@ -2538,6 +2540,58 @@
|
|||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.1.tgz",
|
||||
"integrity": "sha512-0aswLpUKZIraPEOcXfwW25N1KPfLA6Mvm1TxogUChGsbLbys2ihd7uk9XAKsol9ZQPucxh2/mybwdRtAKrs/MQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.0",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.2",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.2.tgz",
|
||||
"integrity": "sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.2",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
|
||||
|
|
@ -4750,8 +4804,7 @@
|
|||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
|
|
@ -9155,6 +9208,48 @@
|
|||
"react-remove-scroll": "2.5.5"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-slider": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.1.tgz",
|
||||
"integrity": "sha512-0aswLpUKZIraPEOcXfwW25N1KPfLA6Mvm1TxogUChGsbLbys2ihd7uk9XAKsol9ZQPucxh2/mybwdRtAKrs/MQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.0",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.2",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-collection": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.2.tgz",
|
||||
"integrity": "sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.2",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
|
||||
|
|
@ -10745,8 +10840,7 @@
|
|||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.12",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fix-date": "^1.1.6",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
|
@ -12,6 +13,7 @@ import organizeRaids from '~utils/organizeRaids'
|
|||
import { setHeaders } from '~utils/userToken'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { defaultFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
|
||||
|
|
@ -82,6 +84,8 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
parse: (query: string) => parseInt(query),
|
||||
serialize: (value) => `${value}`,
|
||||
})
|
||||
const [advancedFilters, setAdvancedFilters] =
|
||||
useState<FilterSet>(defaultFilterset)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
|
|
@ -120,6 +124,16 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Fetch the user's advanced filters
|
||||
useEffect(() => {
|
||||
const filtersCookie = getCookie('filters')
|
||||
const filters = filtersCookie
|
||||
? JSON.parse(filtersCookie as string)
|
||||
: defaultFilterset
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
|
|
@ -138,6 +152,7 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
raid: raid ? raid.id : undefined,
|
||||
recency: recency != -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
...advancedFilters,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +175,7 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
.catch((error) => handleError(error))
|
||||
}
|
||||
},
|
||||
[currentPage, username, parties, element, raid, recency]
|
||||
[currentPage, username, parties, element, raid, recency, advancedFilters]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
|
|
@ -194,7 +209,7 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchProfile({ replace: true })
|
||||
}, [username, element, raid, recency])
|
||||
}, [username, element, raid, recency, advancedFilters])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
|
|
@ -204,25 +219,23 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0, { shallow: true })
|
||||
else if (element) setElement(element, { shallow: true })
|
||||
function receiveFilters(filters: FilterSet) {
|
||||
if (filters.element == 0) setElement(0, { shallow: true })
|
||||
else if (filters.element) setElement(filters.element, { shallow: true })
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
if (raids && filters.raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === filters.raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug, { shallow: true })
|
||||
setRaidSlug(filters.raidSlug, { shallow: true })
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency, { shallow: true })
|
||||
if (filters.recency) setRecency(filters.recency, { shallow: true })
|
||||
|
||||
delete filters.element
|
||||
delete filters.raidSlug
|
||||
delete filters.recency
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
|
|
@ -259,6 +272,7 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
grid={party.weapons}
|
||||
favorited={party.favorited}
|
||||
fullAuto={party.full_auto}
|
||||
autoGuard={party.auto_guard}
|
||||
key={`party-${i}`}
|
||||
onClick={goTo}
|
||||
/>
|
||||
|
|
@ -334,6 +348,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Fetch user's advanced filters
|
||||
const filtersCookie = getCookie('filters', { req: req, res: res })
|
||||
const advancedFilters = filtersCookie
|
||||
? JSON.parse(filtersCookie as string)
|
||||
: undefined
|
||||
|
||||
try {
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
|
|
@ -343,7 +363,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
params: { ...filters, ...advancedFilters },
|
||||
}
|
||||
|
||||
// Set up empty variables
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
|
@ -13,6 +14,7 @@ import fetchLatestVersion from '~utils/fetchLatestVersion'
|
|||
import organizeRaids from '~utils/organizeRaids'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { defaultFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
|
||||
|
|
@ -82,6 +84,8 @@ const SavedRoute: React.FC<Props> = ({
|
|||
parse: (query: string) => parseInt(query),
|
||||
serialize: (value) => `${value}`,
|
||||
})
|
||||
const [advancedFilters, setAdvancedFilters] =
|
||||
useState<FilterSet>(defaultFilterset)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
|
|
@ -120,6 +124,16 @@ const SavedRoute: React.FC<Props> = ({
|
|||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Fetch the user's advanced filters
|
||||
useEffect(() => {
|
||||
const filtersCookie = getCookie('filters')
|
||||
const filters = filtersCookie
|
||||
? JSON.parse(filtersCookie as string)
|
||||
: defaultFilterset
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
|
|
@ -138,6 +152,7 @@ const SavedRoute: React.FC<Props> = ({
|
|||
raid: raid ? raid.id : undefined,
|
||||
recency: recency !== -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
...advancedFilters,
|
||||
}
|
||||
|
||||
Object.keys(filters).forEach(
|
||||
|
|
@ -164,7 +179,7 @@ const SavedRoute: React.FC<Props> = ({
|
|||
})
|
||||
.catch((error) => handleError(error))
|
||||
},
|
||||
[currentPage, parties, element, raid, recency]
|
||||
[currentPage, parties, element, raid, recency, advancedFilters]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
|
|
@ -198,7 +213,7 @@ const SavedRoute: React.FC<Props> = ({
|
|||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [element, raid, recency])
|
||||
}, [element, raid, recency, advancedFilters])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
|
|
@ -208,25 +223,23 @@ const SavedRoute: React.FC<Props> = ({
|
|||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0, { shallow: true })
|
||||
else if (element) setElement(element, { shallow: true })
|
||||
function receiveFilters(filters: FilterSet) {
|
||||
if (filters.element == 0) setElement(0, { shallow: true })
|
||||
else if (filters.element) setElement(filters.element, { shallow: true })
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
if (raids && filters.raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === filters.raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug, { shallow: true })
|
||||
setRaidSlug(filters.raidSlug, { shallow: true })
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency, { shallow: true })
|
||||
if (filters.recency) setRecency(filters.recency, { shallow: true })
|
||||
|
||||
delete filters.element
|
||||
delete filters.raidSlug
|
||||
delete filters.recency
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
|
|
@ -300,6 +313,7 @@ const SavedRoute: React.FC<Props> = ({
|
|||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
fullAuto={party.full_auto}
|
||||
autoGuard={party.auto_guard}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
|
|
@ -368,6 +382,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Fetch user's advanced filters
|
||||
const filtersCookie = getCookie('filters', { req: req, res: res })
|
||||
const advancedFilters = filtersCookie
|
||||
? JSON.parse(filtersCookie as string)
|
||||
: undefined
|
||||
|
||||
try {
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
|
|
@ -377,7 +397,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
params: { ...filters, ...advancedFilters },
|
||||
}
|
||||
|
||||
// Set up empty variables
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
|
@ -13,6 +14,7 @@ import fetchLatestVersion from '~utils/fetchLatestVersion'
|
|||
import organizeRaids from '~utils/organizeRaids'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { defaultFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
|
||||
|
|
@ -82,6 +84,8 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
parse: (query: string) => parseInt(query),
|
||||
serialize: (value) => `${value}`,
|
||||
})
|
||||
const [advancedFilters, setAdvancedFilters] =
|
||||
useState<FilterSet>(defaultFilterset)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
|
|
@ -120,6 +124,16 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Fetch the user's advanced filters
|
||||
useEffect(() => {
|
||||
const filtersCookie = getCookie('filters')
|
||||
const filters = filtersCookie
|
||||
? JSON.parse(filtersCookie as string)
|
||||
: defaultFilterset
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
|
|
@ -138,6 +152,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
raid: raid ? raid.id : undefined,
|
||||
recency: recency !== -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
...advancedFilters,
|
||||
}
|
||||
|
||||
Object.keys(filters).forEach(
|
||||
|
|
@ -164,7 +179,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
})
|
||||
.catch((error) => handleError(error))
|
||||
},
|
||||
[currentPage, parties, element, raid, recency]
|
||||
[currentPage, parties, element, raid, recency, advancedFilters]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
|
|
@ -198,7 +213,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [element, raid, recency])
|
||||
}, [element, raid, recency, advancedFilters])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
|
|
@ -208,25 +223,23 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0, { shallow: true })
|
||||
else if (element) setElement(element, { shallow: true })
|
||||
function receiveFilters(filters: FilterSet) {
|
||||
if (filters.element == 0) setElement(0, { shallow: true })
|
||||
else if (filters.element) setElement(filters.element, { shallow: true })
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
if (raids && filters.raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === filters.raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug, { shallow: true })
|
||||
setRaidSlug(filters.raidSlug, { shallow: true })
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency, { shallow: true })
|
||||
if (filters.recency) setRecency(filters.recency, { shallow: true })
|
||||
|
||||
delete filters.element
|
||||
delete filters.raidSlug
|
||||
delete filters.recency
|
||||
|
||||
setAdvancedFilters(filters)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
|
|
@ -300,6 +313,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
fullAuto={party.full_auto}
|
||||
autoGuard={party.auto_guard}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
|
|
@ -368,6 +382,10 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Fetch user's advanced filters
|
||||
const filtersCookie = getCookie('filters', { req: req, res: res })
|
||||
const advancedFilters = filtersCookie ? JSON.parse(filtersCookie as string) : undefined
|
||||
|
||||
try {
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
|
|
@ -377,7 +395,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
params: { ...filters, ...advancedFilters },
|
||||
}
|
||||
|
||||
// Set up empty variables
|
||||
|
|
|
|||
3
public/icons/Filter.svg
Normal file
3
public/icons/Filter.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 1.5H12C12.2761 1.5 12.5 1.72386 12.5 2V3.46482C12.5 3.63199 12.4164 3.78811 12.2774 3.88084L12.5547 4.29687L12.2773 3.88084L8.66795 6.28711C8.25065 6.56531 8 7.03365 8 7.53518V10.4648C8 10.632 7.91645 10.7881 7.77735 10.8808L6 12.0657V7.53518C6 7.03365 5.74935 6.56531 5.33205 6.28711L1.72265 3.88084C1.58355 3.78811 1.5 3.63199 1.5 3.46482V2C1.5 1.72386 1.72386 1.5 2 1.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 458 B |
|
|
@ -1,3 +1,3 @@
|
|||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.2485 18.9009L23.2738 18.8461L23.2969 18.7903C25.0719 14.5024 25.7609 9.66488 25.9448 6.46207C26.0895 3.94417 24.3044 1.84216 21.9579 1.40085C15.9228 0.265829 11.8484 0.285157 6.02633 1.39844C3.68545 1.84606 1.90969 3.9468 2.05687 6.45935C2.23934 9.57444 2.91185 14.4595 4.70282 18.7895C5.52785 20.7841 6.67748 22.8599 8.06609 24.4951C9.34592 26.0023 11.3727 27.7692 14 27.7692C16.7045 27.7692 18.7211 25.8684 19.9427 24.385C21.2895 22.7497 22.4117 20.7123 23.2485 18.9009Z" />
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 598 B |
|
|
@ -207,6 +207,31 @@
|
|||
"cancel": "Nevermind"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"title": "Advanced filters",
|
||||
"labels": {
|
||||
"charge_attack": "Charge Attack",
|
||||
"full_auto": "Full Auto",
|
||||
"auto_guard": "Auto Guard",
|
||||
"max_buttons": "Maximum number of button presses",
|
||||
"max_turns": "Maximum number of turns",
|
||||
"min_characters": "Minimum number of characters",
|
||||
"min_summons": "Minimum number of summons",
|
||||
"min_weapons": "Minimum number of weapons",
|
||||
"name_quality": "Hide untitled teams",
|
||||
"user_quality": "Hide anonymous users",
|
||||
"original_only": "Hide remixed teams"
|
||||
},
|
||||
"options": {
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"on_and_off": "On and Off"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "Save filters",
|
||||
"clear": "Clear filters"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Log in",
|
||||
"buttons": {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,31 @@
|
|||
"cancel": "キャンセル"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"title": "フィルター設定",
|
||||
"labels": {
|
||||
"charge_attack": "奥義",
|
||||
"full_auto": "フルオート",
|
||||
"auto_guard": "オートガード",
|
||||
"max_buttons": "最大ポチ数",
|
||||
"max_turns": "最大ターン数",
|
||||
"min_characters": "最小キャラクター数",
|
||||
"min_summons": "最小召喚石数",
|
||||
"min_weapons": "最小武器数",
|
||||
"name_quality": "無題の編成なし",
|
||||
"user_quality": "無名のユーザーなし",
|
||||
"original_only": "リミックスなし"
|
||||
},
|
||||
"options": {
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"on_and_off": "両方"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "フィルターを保存する",
|
||||
"clear": "保存したフィルターをクリア"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "ログイン",
|
||||
"buttons": {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
--separator-bg: #{$separator--bg--light};
|
||||
|
||||
--accent-blue: #{$accent--blue--light};
|
||||
--accent-blue-focus: #{$accent--blue--light--focus};
|
||||
|
||||
--accent-yellow: #{$accent--yellow--light};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--light};
|
||||
|
|
@ -64,6 +66,12 @@
|
|||
--select-separator: #{$select--separator--light};
|
||||
--option-bg-hover: #{$option--bg--light--hover};
|
||||
|
||||
// Light - Sliders
|
||||
--slider-thumb-bg: #{$slider--thumb--bg--light};
|
||||
--slider-thumb-bg-hover: #{$slider--thumb--bg--light--hover};
|
||||
--slider-thumb-shadow: #{$slider--thumb--shadow--light};
|
||||
--slider-thumb-shadow-hover: #{$slider--thumb--shadow--light--hover};
|
||||
|
||||
// Light - About
|
||||
--link-item-bg: #{$link--item--bg--light};
|
||||
--link-item-image-color: #{$link--item--bg--image--light};
|
||||
|
|
@ -163,6 +171,8 @@
|
|||
--separator-bg: #{$separator--bg--dark};
|
||||
|
||||
--accent-blue: #{$accent--blue--dark};
|
||||
--accent-blue-focus: #{$accent--blue--dark--focus};
|
||||
|
||||
--accent-yellow: #{$accent--yellow--dark};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--dark};
|
||||
|
|
@ -207,6 +217,12 @@
|
|||
--select-separator: #{$select--separator--dark};
|
||||
--option-bg-hover: #{$option--bg--dark--hover};
|
||||
|
||||
// Dark - Sliders
|
||||
--slider-thumb-bg: #{$slider--thumb--bg--dark};
|
||||
--slider-thumb-bg-hover: #{$slider--thumb--bg--dark--hover};
|
||||
--slider-thumb-shadow: #{$slider--thumb--shadow--dark};
|
||||
--slider-thumb-shadow-hover: #{$slider--thumb--shadow--dark--hover};
|
||||
|
||||
// Dark - About
|
||||
--link-item-bg: #{$link--item--bg--dark};
|
||||
--link-item-image-color: #{$link--item--bg--image--dark};
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ $yellow: #c89d39;
|
|||
$error: #d13a3a;
|
||||
|
||||
$accent--blue--light: #275dc5;
|
||||
$accent--blue--light--focus: #0c398d;
|
||||
$accent--blue--dark: #6195f4;
|
||||
$accent--blue--dark--focus: #275dc5;
|
||||
|
||||
$accent--yellow--light: #c89d39;
|
||||
$accent--yellow--dark: #f9cc64;
|
||||
|
|
@ -224,6 +226,17 @@ $grid--rep--hover--dark: $grey-10;
|
|||
$grid--border--color--light: $grey-80;
|
||||
$grid--border--color--dark: $grey-40;
|
||||
|
||||
// Color Definitions: Slider
|
||||
$slider--thumb--bg--light: $grey-100;
|
||||
$slider--thumb--bg--dark: $grey-100;
|
||||
$slider--thumb--bg--light--hover: $grey-95;
|
||||
$slider--thumb--bg--dark--hover: $grey-95;
|
||||
$slider--thumb--shadow--light: $grey-70;
|
||||
$slider--thumb--shadow--dark: $grey-05;
|
||||
$slider--thumb--shadow--light--hover: $grey-50;
|
||||
$slider--thumb--shadow--dark--hover: $grey-00;
|
||||
|
||||
// Color Definitions: Switch
|
||||
$switch--nub--bg--light: $grey-80;
|
||||
$switch--nub--bg--dark: $grey-30;
|
||||
|
||||
|
|
|
|||
16
types/FilterSet.d.ts
vendored
Normal file
16
types/FilterSet.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
interface FilterSet {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
full_auto?: number
|
||||
auto_guard?: number
|
||||
charge_attack?: number
|
||||
characters_count?: number
|
||||
weapons_count?: number
|
||||
summons_count?: number
|
||||
button_count?: number
|
||||
turn_count?: number
|
||||
name_quality?: boolean
|
||||
user_quality?: boolean
|
||||
original?: boolean
|
||||
}
|
||||
21
utils/defaultFilters.tsx
Normal file
21
utils/defaultFilters.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const DEFAULT_FULL_AUTO = -1
|
||||
const DEFAULT_AUTO_GUARD = -1
|
||||
const DEFAULT_CHARGE_ATTACK = -1
|
||||
const DEFAULT_MIN_CHARACTERS = 3
|
||||
const DEFAULT_MIN_WEAPONS = 5
|
||||
const DEFAULT_MIN_SUMMONS = 2
|
||||
const DEFAULT_NAME_QUALITY = false
|
||||
const DEFAULT_USER_QUALITY = false
|
||||
const DEFAULT_ORIGINAL_ONLY = false
|
||||
|
||||
export const defaultFilterset: FilterSet = {
|
||||
full_auto: DEFAULT_FULL_AUTO,
|
||||
auto_guard: DEFAULT_AUTO_GUARD,
|
||||
charge_attack: DEFAULT_CHARGE_ATTACK,
|
||||
characters_count: DEFAULT_MIN_CHARACTERS,
|
||||
weapons_count: DEFAULT_MIN_WEAPONS,
|
||||
summons_count: DEFAULT_MIN_SUMMONS,
|
||||
name_quality: DEFAULT_NAME_QUALITY,
|
||||
user_quality: DEFAULT_USER_QUALITY,
|
||||
original: DEFAULT_ORIGINAL_ONLY,
|
||||
}
|
||||
Loading…
Reference in a new issue