diff --git a/components/raids/RaidCombobox/index.scss b/components/raids/RaidCombobox/index.scss new file mode 100644 index 00000000..87469f76 --- /dev/null +++ b/components/raids/RaidCombobox/index.scss @@ -0,0 +1,151 @@ +.DetailsWrapper .PartyDetails.Editable .Raid.SelectTrigger { + display: flex; + padding-top: 10px; + padding-bottom: 11px; + min-height: 51px; + + .Value { + display: flex; + gap: $unit-half; + + .Info { + display: flex; + align-items: center; + gap: $unit-half; + flex-grow: 1; + } + + .ExtraIndicator { + background: var(--extra-purple-bg); + border-radius: $full-corner; + color: var(--extra-purple-text); + display: flex; + font-weight: $bold; + font-size: $font-tiny; + width: $unit-3x; + height: $unit-3x; + justify-content: center; + align-items: center; + } + + .Group, + .Separator { + color: var(--text-tertiary); + } + + .Raid.wind { + color: var(--wind-text); + } + + .Raid.fire { + color: var(--fire-text); + } + + .Raid.water { + color: var(--water-text); + } + + .Raid.earth { + color: var(--earth-text); + } + + .Raid.dark { + color: var(--dark-text); + } + + .Raid.light { + color: var(--light-text); + } + } +} + +.Combobox.Raid { + box-sizing: border-box; + /* Temporary values */ + + .Header { + background: var(--dialog-bg); + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: $unit; + padding: $unit; + width: 100%; + + .Controls { + display: flex; + gap: $unit; + + .Button.Blended.small { + padding: $unit ($unit * 1.25); + + &:hover { + background: var(--button-contained-bg); + } + } + + .Flipped { + transform: rotate(180deg); + } + + .SegmentedControlWrapper { + flex-grow: 1; + + .SegmentedControl { + width: 100%; + } + } + } + } + + .Raids { + height: 40vh; + overflow-y: scroll; + padding: 0 $unit; + + &.Searching { + .CommandGroup { + padding-top: 0; + padding-bottom: 0; + + .Label { + display: none; + } + + .SelectItem { + margin: 0; + } + } + + .CommandGroup.Hidden { + display: block; + } + } + + .CommandGroup { + &.Hidden { + display: none; + } + + .Label { + align-items: center; + color: var(--text-tertiary); + display: flex; + flex-direction: row; + flex-shrink: 0; + font-size: $font-small; + font-weight: $medium; + gap: $unit; + padding: $unit $unit-2x $unit-half ($unit * 1.5); + + .Separator { + background: var(--select-separator); + border-radius: 1px; + display: block; + flex-grow: 1; + height: 2px; + } + } + } + } +} diff --git a/components/raids/RaidCombobox/index.tsx b/components/raids/RaidCombobox/index.tsx new file mode 100644 index 00000000..54209987 --- /dev/null +++ b/components/raids/RaidCombobox/index.tsx @@ -0,0 +1,331 @@ +import { createRef, useCallback, useEffect, useRef, useState } from 'react' +import { useRouter } from 'next/router' +import { useTranslation } from 'react-i18next' + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from 'cmdk' +import Popover from '~components/common/Popover' +import SegmentedControl from '~components/common/SegmentedControl' +import Segment from '~components/common/Segment' +import RaidItem from '~components/raids/RaidItem' +import Tooltip from '~components/common/Tooltip' + +import api from '~utils/api' + +interface Props { + showAllRaidsOption: boolean + currentRaid?: Raid + defaultRaid?: Raid + onChange?: (raid?: Raid) => void + onBlur?: (event: React.ChangeEvent) => void +} + +import Button from '~components/common/Button' +import ArrowIcon from '~public/icons/Arrow.svg' + +import './index.scss' +import classNames from 'classnames' +import { appState } from '~utils/appState' + +const NUM_SECTIONS = 3 + +enum Sort { + ASCENDING, + DESCENDING, +} + +const RaidCombobox = (props: Props) => { + // Set up router for locale + const router = useRouter() + const locale = router.locale || 'en' + + // Set up translations + const { t } = useTranslation('common') + + // Component state + const [open, setOpen] = useState(false) + const [sort, setSort] = useState(Sort.DESCENDING) + const [scrolled, setScrolled] = useState(false) + + // Data state + const [currentSection, setCurrentSection] = useState(1) + const [search, setSearch] = useState('') + const [sections, setSections] = useState() + const [currentRaid, setCurrentRaid] = useState() + + // Refs + const listRef = createRef() + const selectedRef = createRef() + + useEffect(() => { + if (appState.party.raid) { + setCurrentRaid(appState.party.raid) + setCurrentSection(appState.party.raid.group.section) + } + }, []) + + // Scroll to the top of the list when the user switches tabs + useEffect(() => { + if (listRef.current) listRef.current.scrollTop = 0 + }, [currentSection]) + + const scrollToItem = useCallback( + (node) => { + if ( + !scrolled && + open && + currentRaid && + listRef.current && + node !== null + ) { + const listRect = listRef.current.getBoundingClientRect() + const itemRect = node.getBoundingClientRect() + const distance = itemRect.top - listRect.top + + listRef.current.scrollTop = distance + setScrolled(true) + } + }, + [open, currentRaid, listRef] + ) + + // Methods: Convenience methods + + function reverseSort() { + if (sort === Sort.ASCENDING) setSort(Sort.DESCENDING) + else setSort(Sort.ASCENDING) + } + + // Sort raids into defined groups + const sortGroups = useCallback( + (groups: RaidGroup[]) => { + const sections: [RaidGroup[], RaidGroup[], RaidGroup[]] = [[], [], []] + + groups.forEach((group) => { + if (group.section > 0) sections[group.section - 1].push(group) + }) + + setSections(sections) + }, + [setSections] + ) + + function handleValueChange(raid: Raid) { + setCurrentRaid(raid) + setOpen(false) + setScrolled(false) + if (props.onChange) props.onChange(raid) + } + + function toggleOpen() { + if (open) { + if (currentRaid) setCurrentSection(currentRaid.group.section) + setScrolled(false) + } + setOpen(!open) + } + + const linkClass = classNames({ + wind: currentRaid && currentRaid.element == 1, + fire: currentRaid && currentRaid.element == 2, + water: currentRaid && currentRaid.element == 3, + earth: currentRaid && currentRaid.element == 4, + dark: currentRaid && currentRaid.element == 5, + light: currentRaid && currentRaid.element == 6, + }) + + // Fetch all raids on mount + useEffect(() => { + api.raidGroups().then((response) => sortGroups(response.data)) + }, [sortGroups]) + + // Methods: Rendering + function renderRaidSections() { + let sections = [] + for (let i = 0; i < NUM_SECTIONS; i++) { + sections.push(renderRaidSection(i)) + } + return sections + } + + function renderRaidSection(section: number) { + if (!sections || !sections[section]) return + else { + const currentSection = sections[section] + return currentSection + .sort((a, b) => { + if (sort === Sort.ASCENDING) return a.order - b.order + else return b.order - a.order + }) + .map((group, i) => renderRaidGroup(section, i)) + } + } + + // Render JSX for each raid option, sorted into optgroups + function renderRaidGroup(section: number, index: number) { + let options = [] + + if (!sections || !sections[section] || !sections[section][index]) return + else { + const group = sections[section][index] + + options = group.raids + .sort((a, b) => { + if (a.element > 0 && b.element > 0) return a.element - b.element + else if (a.name.en.includes('NM') && b.name.en.includes('NM')) + return a.level < b.level ? -1 : 1 + else return a.name.en < b.name.en ? -1 : 1 + }) + .map((item, i) => renderRaidItem(item, i)) + + return ( + + {group.name[locale]} +
+
+ } + > + {options} +
+ ) + } + } + + function renderRaidItem(raid: Raid, key: number) { + return ( + handleValueChange(raid)} + > + {raid.name[locale]} + + ) + } + + return ( + +
+ + {currentRaid?.group.name[locale]} + + / + + {currentRaid?.name[locale]} + +
+ {currentRaid.group.extra ? ( + EX + ) : ( + '' + )} + + ), + rawValue: currentRaid?.id, + } + : undefined + } + > + +
+ + {!search ? ( +
+ + setCurrentSection(2)} + > + {t('raids.sections.events')} + + setCurrentSection(1)} + > + {t('raids.sections.raids')} + + setCurrentSection(3)} + > + {t('raids.sections.solo')} + + + +
+ ) : ( + '' + )} +
+
+ {renderRaidSections()} +
+
+
+ ) +} + +export default RaidCombobox diff --git a/components/raids/RaidItem/index.scss b/components/raids/RaidItem/index.scss new file mode 100644 index 00000000..0a6a0292 --- /dev/null +++ b/components/raids/RaidItem/index.scss @@ -0,0 +1,53 @@ +.SelectItem.Raid { + padding-top: $unit; + padding-bottom: $unit; + padding-left: $unit; + + &:hover { + .ExtraIndicator { + background: var(--extra-purple-primary); + color: white; + } + + .Selected { + background-color: var(--pill-bg-hover); + color: var(--pill-text-hover); + } + } + + .Text { + flex-grow: 1; + } + + .ExtraIndicator { + background: var(--extra-purple-bg); + border-radius: $full-corner; + color: var(--extra-purple-text); + display: flex; + font-weight: $bold; + font-size: $font-tiny; + width: $unit-3x; + height: $unit-3x; + justify-content: center; + align-items: center; + } + + .Selected { + background-color: var(--pill-bg); + color: var(--pill-text); + border-radius: $full-corner; + display: flex; + align-items: center; + font-size: $font-tiny; + font-weight: $bold; + padding: 0 $unit; + height: $unit-3x; + } + + img { + background: var(--input-bound-bg); + border-radius: $unit-half; + width: $unit-10x; + height: 56px; + } +} diff --git a/components/raids/RaidItem/index.tsx b/components/raids/RaidItem/index.tsx new file mode 100644 index 00000000..9fa4e7c4 --- /dev/null +++ b/components/raids/RaidItem/index.tsx @@ -0,0 +1,57 @@ +import React, { ComponentProps, PropsWithChildren } from 'react' +import { CommandItem } from '~components/common/Command' + +import './index.scss' +import classNames from 'classnames' + +interface Props { + className?: string + icon?: { + alt: string + src: string + } + extra: boolean + selected: boolean + value: string | number + onSelect: () => void +} +const RaidItem = React.forwardRef>( + function Item( + { + icon, + value, + extra, + selected, + children, + ...props + }: PropsWithChildren, + forwardedRef + ) { + const classes = classNames( + { SelectItem: true, Raid: true }, + props.className + ) + + return ( + + {icon ? {icon.alt} : ''} + {children} + {selected ? Selected : ''} + {extra ? EX : ''} + + ) + } +) + +RaidItem.defaultProps = { + extra: false, + selected: false, +} + +export default RaidItem