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:
Justin Edmund 2023-04-09 19:40:15 -07:00 committed by GitHub
parent 7b54791bb3
commit 968ae5c41e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1475 additions and 328 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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}
/>
</>
)
}

View 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;
}
}

View 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

View file

@ -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 {

View file

@ -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)}

View file

@ -0,0 +1,4 @@
.InputField.TableField .Input {
text-align: right;
width: $unit-8x;
}

View 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

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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>
)
}

View 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);
}
}

View 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

View file

@ -0,0 +1,8 @@
.SliderField.TableField {
min-height: $unit-4x;
.Input {
text-align: right;
width: $unit-8x;
}
}

View 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

View file

@ -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 {

View file

View 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

View 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;
}
}
}

View 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
View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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": {

View file

@ -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": {

View file

@ -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};

View file

@ -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
View 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
View 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,
}