diff --git a/components/AccountModal/index.scss b/components/AccountModal/index.scss index 47a61c56..e2bbd2e0 100644 --- a/components/AccountModal/index.scss +++ b/components/AccountModal/index.scss @@ -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; diff --git a/components/DialogContent/index.scss b/components/DialogContent/index.scss index 26421dbf..8603f748 100644 --- a/components/DialogContent/index.scss +++ b/components/DialogContent/index.scss @@ -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; diff --git a/components/FilterBar/index.scss b/components/FilterBar/index.scss index e7e074c0..b8bde51e 100644 --- a/components/FilterBar/index.scss +++ b/components/FilterBar/index.scss @@ -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 { diff --git a/components/FilterBar/index.tsx b/components/FilterBar/index.tsx index 426dd856..116d8bf2 100644 --- a/components/FilterBar/index.tsx +++ b/components/FilterBar/index.tsx @@ -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({}) + + 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 ( -
- {props.children} -
- + <> +
+ {props.children} +
+ - + - + + +
-
+ + ) } diff --git a/components/FilterModal/index.scss b/components/FilterModal/index.scss new file mode 100644 index 00000000..6be72ddf --- /dev/null +++ b/components/FilterModal/index.scss @@ -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; + } +} diff --git a/components/FilterModal/index.tsx b/components/FilterModal/index.tsx new file mode 100644 index 00000000..c6a09bb7 --- /dev/null +++ b/components/FilterModal/index.tsx @@ -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() + const footerRef = React.createRef() + + // 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({}) + + // 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 = () => ( + + ) + + const minWeaponsField = () => ( + + ) + + const minSummonsField = () => ( + + ) + + // Selects + const fullAutoField = () => ( + openSelect('full_auto')} + onClose={() => setFullAutoOpen(false)} + onChange={handleFullAutoValueChange} + > + + {t('modals.filters.options.on_and_off')} + + + {t('modals.filters.options.on')} + + + {t('modals.filters.options.off')} + + + ) + + const autoGuardField = () => ( + openSelect('auto_guard')} + onClose={() => setAutoGuardOpen(false)} + onChange={handleAutoGuardValueChange} + > + + {t('modals.filters.options.on_and_off')} + + + {t('modals.filters.options.on')} + + + {t('modals.filters.options.off')} + + + ) + + const chargeAttackField = () => ( + openSelect('charge_attack')} + onClose={() => setChargeAttackOpen(false)} + onChange={handleChargeAttackValueChange} + > + + {t('modals.filters.options.on_and_off')} + + + {t('modals.filters.options.on')} + + + {t('modals.filters.options.off')} + + + ) + + // Switches + const nameQualityField = () => ( + + ) + + const userQualityField = () => ( + + ) + + const originalOnlyField = () => ( + + ) + + // Inputs + const maxButtonsField = () => ( + + ) + + const maxTurnsField = () => ( + + ) + + return ( + + {props.children} + +
+
+ + {t('modals.filters.title')} + +
+ + + + + +
+ +
+ {chargeAttackField()} + {fullAutoField()} + {autoGuardField()} + {/* {maxButtonsField()} */} + {/* {maxTurnsField()} */} + {minCharactersField()} + {minSummonsField()} + {minWeaponsField()} + {nameQualityField()} + {userQualityField()} + {originalOnlyField()} +
+
+
+
+
+
+
+ ) +} + +export default FilterModal diff --git a/components/GridRep/index.scss b/components/GridRep/index.scss index 02a480b0..4d5bd3df 100644 --- a/components/GridRep/index.scss +++ b/components/GridRep/index.scss @@ -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 { diff --git a/components/GridRep/index.tsx b/components/GridRep/index.tsx index f9a50e3f..f2b17fac 100644 --- a/components/GridRep/index.tsx +++ b/components/GridRep/index.tsx @@ -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) => {
) + function fullAutoString() { + const fullAutoElement = ( + + {` · ${t('party.details.labels.full_auto')}`} + + ) + + const autoGuardElement = ( + + + + ) + + return ( +
+ {fullAutoElement} + {props.autoGuard ? autoGuardElement : ''} +
+ ) + } const details = (

{props.name ? props.name : t('no_title')}

@@ -172,13 +200,7 @@ const GridRep = (props: Props) => { {props.raid ? props.raid.name[locale] : t('no_raid')} - {props.fullAuto ? ( - - {` · ${t('party.details.labels.full_auto')}`} - - ) : ( - '' - )} + {props.fullAuto ? fullAutoString() : ''}