diff --git a/.gitignore b/.gitignore index e3403051..9bb7f6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ public/images/accessory* public/images/mastery* public/images/updates* public/images/guidebooks* +public/images/raids* # Typescript v1 declaration files typings/ diff --git a/components/FilterBar/index.scss b/components/FilterBar/index.scss index b8bde51e..5cfb852f 100644 --- a/components/FilterBar/index.scss +++ b/components/FilterBar/index.scss @@ -72,7 +72,7 @@ select, .SelectTrigger { - // background: url("/icons/Arrow.svg"), $grey-90; + // background: url("/icons/Chevron.svg"), $grey-90; // background-repeat: no-repeat; // background-position-y: center; // background-position-x: 95%; diff --git a/components/FilterBar/index.tsx b/components/FilterBar/index.tsx index 11b5332f..5d94deb9 100644 --- a/components/FilterBar/index.tsx +++ b/components/FilterBar/index.tsx @@ -4,7 +4,6 @@ import classNames from 'classnames' import equals from 'fast-deep-equal' import FilterModal from '~components/FilterModal' -import RaidDropdown from '~components/RaidDropdown' import Select from '~components/common/Select' import SelectItem from '~components/common/SelectItem' import Button from '~components/common/Button' @@ -15,6 +14,8 @@ import FilterIcon from '~public/icons/Filter.svg' import './index.scss' import { getCookie } from 'cookies-next' +import RaidCombobox from '~components/raids/RaidCombobox' +import { appState } from '~utils/appState' interface Props { children: React.ReactNode @@ -29,6 +30,8 @@ const FilterBar = (props: Props) => { // Set up translation const { t } = useTranslation('common') + const [currentRaid, setCurrentRaid] = useState() + const [recencyOpen, setRecencyOpen] = useState(false) const [elementOpen, setElementOpen] = useState(false) @@ -47,6 +50,16 @@ const FilterBar = (props: Props) => { FiltersActive: !matchesDefaultFilters, }) + // Convert raid slug to Raid object on mount + useEffect(() => { + const raid = appState.raidGroups + .filter((group) => group.section > 0) + .flatMap((group) => group.raids) + .find((raid) => raid.slug === props.raidSlug) + + setCurrentRaid(raid) + }, [props.raidSlug]) + useEffect(() => { // Fetch user's advanced filters const filtersCookie = getCookie('filters') @@ -76,8 +89,8 @@ const FilterBar = (props: Props) => { props.onFilter({ recency: recencyValue, ...advancedFilters }) } - function raidSelectChanged(slug?: string) { - props.onFilter({ raidSlug: slug, ...advancedFilters }) + function raidSelectChanged(raid?: Raid) { + props.onFilter({ raidSlug: raid?.slug, ...advancedFilters }) } function handleAdvancedFiltersChanged(filters: FilterSet) { @@ -90,6 +103,25 @@ const FilterBar = (props: Props) => { setRecencyOpen(name === 'recency' ? !recencyOpen : false) } + function generateSelectItems() { + const elements = [ + { element: 'all', key: -1, value: -1, text: t('elements.full.all') }, + { element: 'null', key: 0, value: 0, text: t('elements.full.null') }, + { element: 'wind', key: 1, value: 1, text: t('elements.full.wind') }, + { element: 'fire', key: 2, value: 2, text: t('elements.full.fire') }, + { element: 'water', key: 3, value: 3, text: t('elements.full.water') }, + { element: 'earth', key: 4, value: 4, text: t('elements.full.earth') }, + { element: 'dark', key: 5, value: 5, text: t('elements.full.dark') }, + { element: 'light', key: 6, value: 6, text: t('elements.full.light') }, + ] + + return elements.map(({ element, key, value, text }) => ( + + {text} + + )) + } + return ( <>
@@ -97,47 +129,26 @@ const FilterBar = (props: Props) => {
- setOpen(!open)} - onClick={openRaidSelect} - onValueChange={handleChange} - > - {Array.from(Array(sortedRaids?.length)).map((x, i) => - renderRaidGroup(i) - )} - - - ) - } -) - -export default RaidDropdown diff --git a/components/RaidSelect/index.scss b/components/RaidSelect/index.scss new file mode 100644 index 00000000..e6eaa60f --- /dev/null +++ b/components/RaidSelect/index.scss @@ -0,0 +1,22 @@ +.Raid.Select { + min-width: 420px; + + .Top { + display: flex; + flex-direction: column; + gap: $unit; + padding: $unit 0; + + .SegmentedControl { + width: 100%; + } + + .Input.Bound { + background-color: var(--select-contained-bg); + + &:hover { + background-color: var(--select-contained-bg-hover); + } + } + } +} diff --git a/components/RaidSelect/index.tsx b/components/RaidSelect/index.tsx new file mode 100644 index 00000000..8d114b59 --- /dev/null +++ b/components/RaidSelect/index.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import * as RadixSelect from '@radix-ui/react-select' +import classNames from 'classnames' + +import Overlay from '~components/common/Overlay' + +import ChevronIcon from '~public/icons/Chevron.svg' + +import './index.scss' +import SegmentedControl from '~components/common/SegmentedControl' +import Segment from '~components/common/Segment' +import Input from '~components/common/Input' + +// Props +interface Props + extends React.DetailedHTMLProps< + React.SelectHTMLAttributes, + HTMLSelectElement + > { + altText?: string + currentSegment: number + iconSrc?: string + open: boolean + trigger?: React.ReactNode + children?: React.ReactNode + onOpenChange?: () => void + onValueChange?: (value: string) => void + onSegmentClick: (segment: number) => void + onClose?: () => void + triggerClass?: string + overlayVisible?: boolean +} + +const RaidSelect = React.forwardRef(function Select( + props: Props, + forwardedRef +) { + // Import translations + const { t } = useTranslation('common') + + const searchInput = React.createRef() + + const [open, setOpen] = useState(false) + const [value, setValue] = useState('') + const [query, setQuery] = useState('') + + const triggerClasses = classNames( + { + SelectTrigger: true, + Disabled: props.disabled, + }, + props.triggerClass + ) + + useEffect(() => { + setOpen(props.open) + }, [props.open]) + + useEffect(() => { + if (props.value && props.value !== '') setValue(`${props.value}`) + else setValue('') + }, [props.value]) + + function onValueChange(newValue: string) { + setValue(`${newValue}`) + if (props.onValueChange) props.onValueChange(newValue) + } + + function onCloseAutoFocus() { + setOpen(false) + if (props.onClose) props.onClose() + } + + function onEscapeKeyDown() { + setOpen(false) + if (props.onClose) props.onClose() + } + + function onPointerDownOutside() { + setOpen(false) + if (props.onClose) props.onClose() + } + + return ( + + + {props.iconSrc ? {props.altText} : ''} + + {!props.disabled ? ( + + + + ) : ( + '' + )} + + + + <> + + + +
+ {}} + /> + + props.onSegmentClick(1)} + > + {t('raids.sections.events')} + + props.onSegmentClick(0)} + > + {t('raids.sections.raids')} + + props.onSegmentClick(2)} + > + {t('raids.sections.solo')} + + +
+ {props.children} +
+ +
+
+ ) +}) + +RaidSelect.defaultProps = { + overlayVisible: true, +} + +export default RaidSelect diff --git a/components/common/Button/index.scss b/components/common/Button/index.scss index ce7399b7..ac4d42d0 100644 --- a/components/common/Button/index.scss +++ b/components/common/Button/index.scss @@ -1,7 +1,7 @@ .Button { align-items: center; background: var(--button-bg); - border: none; + border: 2px solid transparent; border-radius: $input-corner; color: var(--button-text); display: inline-flex; diff --git a/components/common/CharLimitedFieldset/index.scss b/components/common/CharLimitedFieldset/index.scss index 446453f1..e69de29b 100644 --- a/components/common/CharLimitedFieldset/index.scss +++ b/components/common/CharLimitedFieldset/index.scss @@ -1,38 +0,0 @@ -.Limited { - $offset: 2px; - - align-items: center; - background: var(--input-bg); - border-radius: $input-corner; - border: $offset solid transparent; - box-sizing: border-box; - display: flex; - gap: $unit; - padding-top: 2px; - padding-bottom: 2px; - padding-right: calc($unit-2x - $offset); - - &:focus-within { - border: $offset solid $blue; - // box-shadow: 0 2px rgba(255, 255, 255, 1); - } - - .Counter { - color: $grey-55; - font-weight: $bold; - line-height: 42px; - } - - .Input { - background: transparent; - border: none; - border-radius: 0; - padding: $unit * 1.5 $unit-2x; - padding-left: calc($unit-2x - $offset); - - &:focus { - border: none; - outline: none; - } - } -} diff --git a/components/common/CharLimitedFieldset/index.tsx b/components/common/CharLimitedFieldset/index.tsx index cb822299..16cbef3e 100644 --- a/components/common/CharLimitedFieldset/index.tsx +++ b/components/common/CharLimitedFieldset/index.tsx @@ -32,7 +32,7 @@ const CharLimitedFieldset = React.forwardRef( return (
-
+
, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + {children} + + + ) +} + +const CommandInput = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = 'CommandShortcut' + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/common/Popover/index.scss b/components/common/Popover/index.scss new file mode 100644 index 00000000..79d7b808 --- /dev/null +++ b/components/common/Popover/index.scss @@ -0,0 +1,30 @@ +.Popover { + animation: scaleIn $duration-zoom ease-out; + background: var(--dialog-bg); + border-radius: $card-corner; + border: 0.5px solid rgba(0, 0, 0, 0.18); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); + outline: none; + padding: $unit; + transform-origin: var(--radix-popover-content-transform-origin); + width: var(--radix-popover-trigger-width); + min-width: 440px; + z-index: 5; + + @include breakpoint(phone) { + min-width: auto; + } + + &.Flush { + padding: 0; + } + + .Arrow { + fill: var(--dialog-bg); + filter: drop-shadow(0px 1px 1px rgb(0 0 0 / 0.18)); + margin-top: -1px; + } +} + +[data-radix-popper-content-wrapper] { +} diff --git a/components/common/Popover/index.tsx b/components/common/Popover/index.tsx new file mode 100644 index 00000000..f334b6a7 --- /dev/null +++ b/components/common/Popover/index.tsx @@ -0,0 +1,106 @@ +import React, { + ComponentProps, + PropsWithChildren, + ReactNode, + useEffect, + useState, +} from 'react' +import classNames from 'classnames' + +import * as PopoverPrimitive from '@radix-ui/react-popover' +import ChevronIcon from '~public/icons/Chevron.svg' + +import './index.scss' + +interface Props extends ComponentProps<'div'> { + open: boolean + disabled?: boolean + icon?: { + src: string + alt: string + } + trigger?: { + className?: string + placeholder?: string + } + value?: { + element: ReactNode + rawValue: string + } + onOpenChange?: () => void +} + +const Popover = React.forwardRef(function Popover( + { children, ...props }: PropsWithChildren, + forwardedRef +) { + // Component state + const [open, setOpen] = useState(false) + + // Element classes + const triggerClasses = classNames( + { + SelectTrigger: true, + Disabled: props.disabled, + }, + props.trigger?.className + ) + + // Hooks + useEffect(() => { + setOpen(props.open) + }, [props.open]) + + // Elements + const value = props.value ? ( + + {props.value?.element} + + ) : ( + {props.placeholder} + ) + + const icon = props.icon ? ( + {props.icon.alt} + ) : ( + '' + ) + + const arrow = !props.disabled ? ( + + + + ) : ( + '' + ) + + return ( + + + {icon} + {value} + {arrow} + + + {children} + + + ) +}) + +Popover.defaultProps = { + disabled: false, +} + +export default Popover diff --git a/components/common/PopoverContent/index.scss b/components/common/PopoverContent/index.scss index eb2f80c9..e69de29b 100644 --- a/components/common/PopoverContent/index.scss +++ b/components/common/PopoverContent/index.scss @@ -1,17 +0,0 @@ -.Popover { - animation: scaleIn $duration-zoom ease-out; - background: var(--dialog-bg); - border-radius: $card-corner; - border: 0.5px solid rgba(0, 0, 0, 0.18); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); - outline: none; - padding: $unit; - transform-origin: var(--radix-popover-content-transform-origin); - z-index: 5; - - .Arrow { - fill: var(--dialog-bg); - filter: drop-shadow(0px 1px 1px rgb(0 0 0 / 0.18)); - margin-top: -1px; - } -} diff --git a/components/common/Segment/index.scss b/components/common/Segment/index.scss index fa148d4e..cb0aa7e8 100644 --- a/components/common/Segment/index.scss +++ b/components/common/Segment/index.scss @@ -1,6 +1,7 @@ .Segment { color: $grey-55; cursor: pointer; + flex-grow: 1; font-size: 1.4rem; font-weight: $normal; min-width: 100px; diff --git a/components/common/Segment/index.tsx b/components/common/Segment/index.tsx index 3135f88f..cfc0b3f6 100644 --- a/components/common/Segment/index.tsx +++ b/components/common/Segment/index.tsx @@ -6,11 +6,20 @@ interface Props { groupName: string name: string selected: boolean + tabIndex?: number children: string onClick: (event: React.ChangeEvent) => void } const Segment: React.FC = (props: Props) => { + // Selects the segment when the user presses the spacebar + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === ' ') { + event.preventDefault() + event.currentTarget.click() + } + } + return (
= (props: Props) => { checked={props.selected} onChange={props.onClick} /> - +
) } diff --git a/components/common/SegmentedControl/index.scss b/components/common/SegmentedControl/index.scss index ad209992..6a357216 100644 --- a/components/common/SegmentedControl/index.scss +++ b/components/common/SegmentedControl/index.scss @@ -21,6 +21,15 @@ border-radius: 100px; } + &.Blended { + background: var(--input-bound-bg); + border-radius: $full-corner; + + .Segment input:checked + label { + background: var(--card-bg); + } + } + &.fire { .Segment input:checked + label { background: var(--fire-bg); diff --git a/components/common/SegmentedControl/index.tsx b/components/common/SegmentedControl/index.tsx index 379f18d2..edb6534f 100644 --- a/components/common/SegmentedControl/index.tsx +++ b/components/common/SegmentedControl/index.tsx @@ -1,19 +1,38 @@ import React from 'react' - +import classNames from 'classnames' import './index.scss' interface Props { + className?: string elementClass?: string + blended?: boolean + tabIndex?: number } -const SegmentedControl: React.FC = ({ elementClass, children }) => { +const SegmentedControl: React.FC = ({ + className, + elementClass, + blended, + tabIndex, + children, +}) => { + const classes = classNames( + { + SegmentedControl: true, + Blended: blended, + }, + className, + elementClass + ) return ( -
-
- {children} -
+
+
{children}
) } +SegmentedControl.defaultProps = { + blended: false, +} + export default SegmentedControl diff --git a/components/common/Select/index.scss b/components/common/Select/index.scss index 19426a62..170dba71 100644 --- a/components/common/Select/index.scss +++ b/components/common/Select/index.scss @@ -2,7 +2,7 @@ align-items: center; background-color: var(--input-bg); border-radius: $input-corner; - border: none; + border: 2px solid transparent; display: flex; gap: $unit; padding: ($unit * 1.5) $unit-2x; @@ -34,7 +34,7 @@ cursor: not-allowed; } - &[data-placeholder] > span:not(.SelectIcon) { + &[data-placeholder='true'] > span:not(.SelectIcon) { color: var(--text-secondary); } @@ -73,13 +73,13 @@ } .Select { - background: var(--select-bg); - border-radius: $input-corner; + background: var(--dialog-bg); + border-radius: $card-corner; border: $hover-stroke; box-shadow: $hover-shadow; padding: 0 $unit; z-index: 40; - + min-width: var(--radix-select-trigger-width); .Scroll.Up, .Scroll.Down { padding: $unit 0; diff --git a/components/common/Select/index.tsx b/components/common/Select/index.tsx index d633b155..b36d3503 100644 --- a/components/common/Select/index.tsx +++ b/components/common/Select/index.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import Overlay from '~components/common/Overlay' -import ArrowIcon from '~public/icons/Arrow.svg' +import ChevronIcon from '~public/icons/Chevron.svg' import './index.scss' @@ -86,7 +86,7 @@ const Select = React.forwardRef(function Select( {!props.disabled ? ( - + ) : ( '' @@ -102,16 +102,18 @@ const Select = React.forwardRef(function Select( - + {props.children} - + diff --git a/components/common/SelectItem/index.scss b/components/common/SelectItem/index.scss index a7efa35f..ce481d8a 100644 --- a/components/common/SelectItem/index.scss +++ b/components/common/SelectItem/index.scss @@ -8,7 +8,8 @@ font-size: $font-regular; padding: ($unit * 1.5) $unit-2x; - &:hover { + &:hover, + &:focus { background-color: var(--option-bg-hover); color: var(--text-primary); cursor: pointer; diff --git a/components/common/SelectItem/index.tsx b/components/common/SelectItem/index.tsx index 60b3e4e3..79b7ddef 100644 --- a/components/common/SelectItem/index.tsx +++ b/components/common/SelectItem/index.tsx @@ -17,8 +17,8 @@ const SelectItem = React.forwardRef(function selectItem( const { altText, iconSrc, ...rest } = props return ( diff --git a/components/common/Tooltip/index.scss b/components/common/Tooltip/index.scss index 06c05070..a0333340 100644 --- a/components/common/Tooltip/index.scss +++ b/components/common/Tooltip/index.scss @@ -3,6 +3,7 @@ animation: scaleIn $duration-zoom ease-out; background: var(--dialog-bg); border-radius: $input-corner; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); color: var(--text-tertiary); font-size: $font-tiny; font-weight: $medium; diff --git a/components/party/Party/index.tsx b/components/party/Party/index.tsx index 042c8ad2..a50da9a6 100644 --- a/components/party/Party/index.tsx +++ b/components/party/Party/index.tsx @@ -29,7 +29,6 @@ import './index.scss' interface Props { new?: boolean team?: Party - raids: Raid[][] selectedTab: GridType pushHistory?: (path: string) => void } diff --git a/components/party/PartyHeader/index.tsx b/components/party/PartyHeader/index.tsx index e45a67c7..33f806e0 100644 --- a/components/party/PartyHeader/index.tsx +++ b/components/party/PartyHeader/index.tsx @@ -9,7 +9,7 @@ import Button from '~components/common/Button' import CharLimitedFieldset from '~components/common/CharLimitedFieldset' import DurationInput from '~components/common/DurationInput' import Input from '~components/common/Input' -import RaidDropdown from '~components/RaidDropdown' +import RaidCombobox from '~components/raids/RaidCombobox' import Switch from '~components/common/Switch' import Tooltip from '~components/common/Tooltip' import Token from '~components/common/Token' @@ -227,8 +227,8 @@ const PartyHeader = (props: Props) => { setOpen(!open) } - function receiveRaid(slug?: string) { - if (slug) setRaidSlug(slug) + function receiveRaid(raid?: Raid) { + if (raid) setRaidSlug(raid?.slug) } function switchValue(value: boolean) { @@ -260,7 +260,8 @@ const PartyHeader = (props: Props) => { function updateDetails(event: React.MouseEvent) { const descriptionValue = descriptionInput.current?.value - const raid = raids.find((raid) => raid.slug === raidSlug) + const allRaids = appState.raidGroups.flatMap((group) => group.raids) + const raid = allRaids.find((raid) => raid.slug === raidSlug) const details: DetailsObject = { fullAuto: fullAuto, @@ -498,9 +499,9 @@ const PartyHeader = (props: Props) => { error={errors.name} ref={nameInput} /> -
    @@ -650,7 +651,7 @@ const PartyHeader = (props: Props) => {
{renderUserBlock()} - {party.raid ? linkedRaidBlock(party.raid) : ''} + {appState.party.raid ? linkedRaidBlock(appState.party.raid) : ''} {party.created_at != '' ? (