Compare commits

...

74 commits

Author SHA1 Message Date
7f8530cbfa Fix build errors 2023-04-09 17:52:03 -07:00
ee548ea254 Disable max buttons and turns 2023-04-09 17:05:35 -07:00
eeb0a066fd Merge branch 'staging' into advanced-filters 2023-04-02 03:47:57 -07:00
3ac331a316 Merge branch 'main' into staging 2023-04-02 01:31:15 -07:00
5a41d503d0 Merge branch 'staging' of github.com:jedmund/hensei-web into staging 2023-04-02 01:19:17 -07:00
36408ede7e 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
2023-04-01 12:23:11 -07:00
d5b7fc584c 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
2023-04-01 12:23:11 -07:00
4c6830f049
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
2023-04-01 12:20:57 -07:00
d35cedaf04
Merge branch 'main' into staging 2023-03-31 12:03:37 -07:00
b53a261866
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
2023-03-31 11:55:16 -07:00
ca0d14b5d6
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
2023-03-31 11:41:04 -07:00
00cea70ee3 Add auto guard icon to GridRep 2023-03-21 21:31:44 -07:00
3bae907074 Ensure fetchTeams callback is updated with filters 2023-03-21 21:31:14 -07:00
2c933570a5 Fix auto guard text 2023-03-21 20:37:17 -07:00
4e1d342ff5 Add advanced filter support to saved and profile pages 2023-03-21 20:17:21 -07:00
a259e976ab Fix bad merge 2023-03-21 19:46:36 -07:00
d623431386 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
2023-03-21 19:34:24 -07:00
7b633a6c25 Propagate filters from modal
This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active.
2023-03-21 19:34:24 -07:00
d536c7834d Remove default filterset
This was moved to a utils/ file
2023-03-21 19:34:09 -07:00
87cbd00ac2 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.
2023-03-21 19:34:09 -07:00
ef8401cb14 Populate values from defaultFilterSet 2023-03-21 19:33:42 -07:00
401506dcf8 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)
2023-03-21 19:33:22 -07:00
78e47f513b Add style for filter button with filters active 2023-03-21 19:33:03 -07:00
9769c43e6b Set width of Select in table field in Filter dialog 2023-03-21 19:33:03 -07:00
d0c1f07422 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
2023-03-21 19:33:03 -07:00
84e7fc5ffc Change types and add default filterset object 2023-03-21 19:33:03 -07:00
b5cc3f0682 Add new localizations 2023-03-21 19:33:03 -07:00
88e830dd78 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
2023-03-21 19:33:03 -07:00
e418b64847 Create FilterSet.d.ts 2023-03-21 19:32:20 -07:00
d916658072 Update TableFields to not error
Also optional value is required
2023-03-21 19:32:09 -07:00
388e8ff49e Fix maximum cycle depth exceeded error 2023-03-21 19:31:39 -07:00
d2c40c1d0d Added default values, clearing filters, etc
* Default values
* Ability to clear filters
* Receiving values from components
2023-03-21 19:31:02 -07:00
18aa3d4a4e Added value reporting and fixed a cycle error 2023-03-21 19:29:52 -07:00
a62db832eb Update styles for various components
Added some new colors and fixed spacing
2023-03-21 19:29:31 -07:00
973e5acc34 Added localizations for Advanced filters 2023-03-21 19:28:39 -07:00
e4eb90aff1 Update modal skeleton 2023-03-21 19:28:39 -07:00
4b8e905630 Implement InputTableField 2023-03-21 19:27:17 -07:00
d1dbcab005 Implemented SwitchTableField 2023-03-21 19:27:03 -07:00
914f8929ed Implement SliderTableField 2023-03-21 19:26:51 -07:00
3b3e2d50b3 Implement custom Slider component
This inherits from Radix's Slider
2023-03-21 19:26:22 -07:00
a1152e5827 Make generic TableField and move styles
This is so we have a base for other table rows that use different interactive elements
2023-03-21 19:25:49 -07:00
e3bdf820b4 Add skeleton of FilterModal 2023-03-21 19:25:11 -07:00
e13c07d57f Implement advanced filters on Teams page 2023-03-21 19:20:05 -07:00
43e615fdf3 Propagate filters from modal
This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active.
2023-03-21 19:20:05 -07:00
61e17f655c Remove default filterset
This was moved to a utils/ file
2023-03-21 19:20:05 -07:00
b4464be30c 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.
2023-03-21 19:20:05 -07:00
3a2504a70e Populate values from defaultFilterSet 2023-03-21 19:20:05 -07:00
a6cde5ebcd 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)
2023-03-21 19:20:05 -07:00
5ed05bc5b4 Add style for filter button with filters active 2023-03-21 19:20:05 -07:00
d4e70d9807 Set width of Select in table field in Filter dialog 2023-03-21 19:20:05 -07:00
b0a4bf8fc4 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
2023-03-21 19:20:05 -07:00
8ab65a98e9 Add fast-deep-equal package 2023-03-21 19:20:05 -07:00
062767549b Change types and add default filterset object 2023-03-21 19:20:05 -07:00
84870dceb2 Add new localizations 2023-03-21 19:20:05 -07:00
591b16c880 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
2023-03-21 19:20:05 -07:00
0b861146e4 Create FilterSet.d.ts 2023-03-21 19:20:05 -07:00
e36de8389e Update TableFields to not error
Also optional value is required
2023-03-21 19:20:05 -07:00
e67a8aa359 Fix maximum cycle depth exceeded error 2023-03-21 19:20:05 -07:00
bc4c870d72 Added default values, clearing filters, etc
* Default values
* Ability to clear filters
* Receiving values from components
2023-03-21 19:20:05 -07:00
fe3ef4129c Added value reporting and fixed a cycle error 2023-03-21 19:20:05 -07:00
87390bc07c Update styles for various components
Added some new colors and fixed spacing
2023-03-21 19:20:05 -07:00
6ec0eb6351 Added localizations for Advanced filters 2023-03-21 19:20:05 -07:00
5900672089 Update modal skeleton 2023-03-21 19:20:05 -07:00
ece5e2434c Implement InputTableField 2023-03-21 19:20:05 -07:00
0418c01bc4 Change enabled switch color 2023-03-21 19:20:05 -07:00
2bf578fec9 Implemented SwitchTableField 2023-03-21 19:20:05 -07:00
b67c3bc8b5 Implement SliderTableField 2023-03-21 19:20:05 -07:00
9b7b54b562 Implement custom Slider component
This inherits from Radix's Slider
2023-03-21 19:20:05 -07:00
82ec214b5d Make generic TableField and move styles
This is so we have a base for other table rows that use different interactive elements
2023-03-21 19:20:05 -07:00
1e59d767ba Move AccountModal styles to more generic place 2023-03-21 19:20:05 -07:00
c71b926316 Install react-slider from Radix 2023-03-21 19:20:05 -07:00
ef1f8e83d8 Add skeleton of FilterModal 2023-03-21 19:20:05 -07:00
b1236a1f97
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
2023-03-21 17:45:41 -07:00
3690bcf2a5 Added avatars 2023-03-20 15:05:13 -07:00
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,81 +91,97 @@ const FilterBar = (props: Props) => {
}
return (
<div className={classes}>
{props.children}
<div className="Filters">
<Select
value={`${props.element}`}
open={elementOpen}
onOpenChange={() => onSelectChange('element')}
onValueChange={elementSelectChanged}
onClick={openElementSelect}
>
<SelectItem data-element="all" key={-1} value={-1}>
{t('elements.full.all')}
</SelectItem>
<SelectItem data-element="null" key={0} value={0}>
{t('elements.full.null')}
</SelectItem>
<SelectItem data-element="wind" key={1} value={1}>
{t('elements.full.wind')}
</SelectItem>
<SelectItem data-element="fire" key={2} value={2}>
{t('elements.full.fire')}
</SelectItem>
<SelectItem data-element="water" key={3} value={3}>
{t('elements.full.water')}
</SelectItem>
<SelectItem data-element="earth" key={4} value={4}>
{t('elements.full.earth')}
</SelectItem>
<SelectItem data-element="dark" key={5} value={5}>
{t('elements.full.dark')}
</SelectItem>
<SelectItem data-element="light" key={6} value={6}>
{t('elements.full.light')}
</SelectItem>
</Select>
<>
<div className={classes}>
{props.children}
<div className="Filters">
<Select
value={`${props.element}`}
open={elementOpen}
onOpenChange={() => onSelectChange('element')}
onValueChange={elementSelectChanged}
onClick={openElementSelect}
>
<SelectItem data-element="all" key={-1} value={-1}>
{t('elements.full.all')}
</SelectItem>
<SelectItem data-element="null" key={0} value={0}>
{t('elements.full.null')}
</SelectItem>
<SelectItem data-element="wind" key={1} value={1}>
{t('elements.full.wind')}
</SelectItem>
<SelectItem data-element="fire" key={2} value={2}>
{t('elements.full.fire')}
</SelectItem>
<SelectItem data-element="water" key={3} value={3}>
{t('elements.full.water')}
</SelectItem>
<SelectItem data-element="earth" key={4} value={4}>
{t('elements.full.earth')}
</SelectItem>
<SelectItem data-element="dark" key={5} value={5}>
{t('elements.full.dark')}
</SelectItem>
<SelectItem data-element="light" key={6} value={6}>
{t('elements.full.light')}
</SelectItem>
</Select>
<RaidDropdown
currentRaid={props.raidSlug}
defaultRaid="all"
showAllRaidsOption={true}
onChange={raidSelectChanged}
/>
<RaidDropdown
currentRaid={props.raidSlug}
defaultRaid="all"
showAllRaidsOption={true}
onChange={raidSelectChanged}
/>
<Select
value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen}
onOpenChange={() => onSelectChange('recency')}
onValueChange={recencySelectChanged}
onClick={openRecencySelect}
>
<SelectItem key={-1} value={-1}>
{t('recency.all_time')}
</SelectItem>
<SelectItem key={86400} value={86400}>
{t('recency.last_day')}
</SelectItem>
<SelectItem key={604800} value={604800}>
{t('recency.last_week')}
</SelectItem>
<SelectItem key={2629746} value={2629746}>
{t('recency.last_month')}
</SelectItem>
<SelectItem key={7889238} value={7889238}>
{t('recency.last_3_months')}
</SelectItem>
<SelectItem key={15778476} value={15778476}>
{t('recency.last_6_months')}
</SelectItem>
<SelectItem key={31556952} value={31556952}>
{t('recency.last_year')}
</SelectItem>
</Select>
<Select
value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen}
onOpenChange={() => onSelectChange('recency')}
onValueChange={recencySelectChanged}
onClick={openRecencySelect}
>
<SelectItem key={-1} value={-1}>
{t('recency.all_time')}
</SelectItem>
<SelectItem key={86400} value={86400}>
{t('recency.last_day')}
</SelectItem>
<SelectItem key={604800} value={604800}>
{t('recency.last_week')}
</SelectItem>
<SelectItem key={2629746} value={2629746}>
{t('recency.last_month')}
</SelectItem>
<SelectItem key={7889238} value={7889238}>
{t('recency.last_3_months')}
</SelectItem>
<SelectItem key={15778476} value={15778476}>
{t('recency.last_6_months')}
</SelectItem>
<SelectItem key={31556952} value={31556952}>
{t('recency.last_year')}
</SelectItem>
</Select>
<Button
className={filterButtonClasses}
blended={true}
leftAccessoryIcon={<FilterIcon />}
onClick={() => setFilterModalOpen(true)}
/>
</div>
</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,46 +28,27 @@ 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>
<Select
name={props.name}
open={props.open}
onOpenChange={props.onOpenChange}
onValueChange={props.onChange}
onClose={props.onClose}
triggerClass={classNames({ Bound: true, Table: true })}
value={value}
overlayVisible={false}
>
{props.children}
</Select>
</div>
</div>
<TableField
name={props.name}
imageAlt={props.imageAlt}
imageClass={props.imageClass}
imageSrc={props.imageSrc}
label={props.label}
>
<Select
name={props.name}
open={props.open}
onOpenChange={props.onOpenChange}
onValueChange={props.onChange}
onClose={props.onClose}
triggerClass={classNames({ Bound: true, Table: true })}
value={value}
overlayVisible={false}
>
{props.children}
</Select>
</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}
@ -367,6 +381,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
@ -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,
}