ChatGPT helped me refactor RaidCombobox

This commit is contained in:
Justin Edmund 2023-06-06 08:32:28 -07:00
parent 121b75f63a
commit f96d78770a

View file

@ -20,7 +20,9 @@ import api from '~utils/api'
interface Props { interface Props {
showAllRaidsOption: boolean showAllRaidsOption: boolean
currentRaid?: Raid currentRaid?: Raid
currentRaidSlug?: string
defaultRaid?: Raid defaultRaid?: Raid
minimal?: boolean
onChange?: (raid?: Raid) => void onChange?: (raid?: Raid) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
} }
@ -55,13 +57,19 @@ const RaidCombobox = (props: Props) => {
// Data state // Data state
const [currentSection, setCurrentSection] = useState(1) const [currentSection, setCurrentSection] = useState(1)
const [search, setSearch] = useState('') const [query, setQuery] = useState('')
const [sections, setSections] = useState<RaidGroup[][]>() const [sections, setSections] = useState<RaidGroup[][]>()
const [currentRaid, setCurrentRaid] = useState<Raid>() const [currentRaid, setCurrentRaid] = useState<Raid>()
// Refs // Refs
const listRef = createRef<HTMLDivElement>() const listRef = createRef<HTMLDivElement>()
const selectedRef = createRef<HTMLDivElement>()
function slugToRaid(slug: string) {
return appState.raidGroups
.filter((group) => group.section > 0)
.flatMap((group) => group.raids)
.find((raid) => raid.slug === slug)
}
useEffect(() => { useEffect(() => {
if (appState.party.raid) { if (appState.party.raid) {
@ -70,6 +78,12 @@ const RaidCombobox = (props: Props) => {
} }
}, []) }, [])
useEffect(() => {
if (props.currentRaidSlug) {
setCurrentRaid(slugToRaid(props.currentRaidSlug))
}
})
// Scroll to the top of the list when the user switches tabs // Scroll to the top of the list when the user switches tabs
useEffect(() => { useEffect(() => {
if (listRef.current) listRef.current.scrollTop = 0 if (listRef.current) listRef.current.scrollTop = 0
@ -77,22 +91,15 @@ const RaidCombobox = (props: Props) => {
const scrollToItem = useCallback( const scrollToItem = useCallback(
(node) => { (node) => {
if ( if (!scrolled && open && currentRaid && listRef.current && node) {
!scrolled && const { top: listTop } = listRef.current.getBoundingClientRect()
open && const { top: itemTop } = node.getBoundingClientRect()
currentRaid &&
listRef.current &&
node !== null
) {
const listRect = listRef.current.getBoundingClientRect()
const itemRect = node.getBoundingClientRect()
const distance = itemRect.top - listRect.top
listRef.current.scrollTop = distance listRef.current.scrollTop = itemTop - listTop
setScrolled(true) setScrolled(true)
} }
}, },
[open, currentRaid, listRef] [scrolled, open, currentRaid, listRef]
) )
// Methods: Convenience methods // Methods: Convenience methods
@ -132,7 +139,7 @@ const RaidCombobox = (props: Props) => {
} }
function clearSearch() { function clearSearch() {
setSearch('') setQuery('')
} }
const linkClass = classNames({ const linkClass = classNames({
@ -150,79 +157,81 @@ const RaidCombobox = (props: Props) => {
}, [sortGroups]) }, [sortGroups])
// Methods: Rendering // Methods: Rendering
function renderRaidSections() { function renderRaidSections() {
let sections = [] // Renders each raid section
for (let i = 0; i < NUM_SECTIONS; i++) { return Array.from({ length: NUM_SECTIONS }, (_, i) => renderRaidSection(i))
sections.push(renderRaidSection(i))
}
return sections
} }
function renderRaidSection(section: number) { function renderRaidSection(section: number) {
if (!sections || !sections[section]) return // Renders the specified raid section
else { const currentSection = sections?.[section]
const currentSection = sections[section] if (!currentSection) return
return currentSection
.sort((a, b) => { const sortedGroups = currentSection.sort((a, b) => {
if (sort === Sort.ASCENDING) return a.order - b.order return sort === Sort.ASCENDING ? a.order - b.order : b.order - a.order
else return b.order - a.order })
})
.map((group, i) => renderRaidGroup(section, i)) return sortedGroups.map((group, i) => renderRaidGroup(section, i))
}
} }
// Render JSX for each raid option, sorted into optgroups
function renderRaidGroup(section: number, index: number) { function renderRaidGroup(section: number, index: number) {
let options = [] // Renders the specified raid group
if (!sections?.[section]?.[index]) return
if (!sections || !sections[section] || !sections[section][index]) return const group = sections[section][index]
else { const options = generateRaidItems(group.raids)
const group = sections[section][index]
options = group.raids const groupClassName = classNames({
.sort((a, b) => { CommandGroup: true,
if (a.element > 0 && b.element > 0) return a.element - b.element Hidden: group.section !== currentSection,
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 ( const heading = (
<CommandGroup <div className="Label">
data-section={group.section} {group.name[locale]}
className={classNames({ <div className="Separator" />
CommandGroup: true, </div>
Hidden: group.section !== currentSection, )
})}
key={group.name[locale].toLowerCase().replace(' ', '-')} return (
heading={ <CommandGroup
<div className="Label"> data-section={group.section}
{group.name[locale]} className={groupClassName}
<div className="Separator" /> key={group.name[locale].toLowerCase().replace(' ', '-')}
</div> heading={heading}
} >
> {options}
{options} </CommandGroup>
</CommandGroup> )
) }
}
function generateRaidItems(raids: Raid[]) {
// Generates a list of RaidItem components from the specified raids
return raids
.sort((a, b) => {
if (a.element > 0 && b.element > 0) return a.element - b.element
if (a.name.en.includes('NM') && b.name.en.includes('NM'))
return a.level - b.level
return a.name.en.localeCompare(b.name.en)
})
.map((item, i) => renderRaidItem(item, i))
} }
function renderRaidItem(raid: Raid, key: number) { function renderRaidItem(raid: Raid, key: number) {
// Renders a RaidItem component for the specified raid
const isSelected = currentRaid?.id === raid.id
const isRef = isSelected ? scrollToItem : undefined
const imageUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/raids/${raid.slug}.png`
return ( return (
<RaidItem <RaidItem
className={currentRaid && currentRaid.id === raid.id ? 'Selected' : ''} className={isSelected ? 'Selected' : ''}
icon={{ icon={{ alt: raid.name[locale], src: imageUrl }}
alt: raid.name[locale],
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/raids/${raid.slug}.png`,
}}
extra={raid.group.extra} extra={raid.group.extra}
key={key} key={key}
selected={currentRaid?.id === raid.id} selected={isSelected}
ref={ ref={isRef}
currentRaid && currentRaid.id === raid.id ? scrollToItem : undefined
}
value={raid.slug} value={raid.slug}
onSelect={() => handleValueChange(raid)} onSelect={() => handleValueChange(raid)}
> >
@ -231,6 +240,113 @@ const RaidCombobox = (props: Props) => {
) )
} }
function renderSegmentedControl() {
// Renders a SegmentedControl component for selecting raid sections.
return (
<SegmentedControl blended={true}>
<Segment
groupName="raid_section"
name="events"
selected={currentSection === 2}
onClick={() => setCurrentSection(2)}
>
{t('raids.sections.events')}
</Segment>
<Segment
groupName="raid_section"
name="raids"
selected={currentSection === 1}
onClick={() => setCurrentSection(1)}
>
{t('raids.sections.raids')}
</Segment>
<Segment
groupName="raid_section"
name="solo"
selected={currentSection === 3}
onClick={() => setCurrentSection(3)}
>
{t('raids.sections.solo')}
</Segment>
</SegmentedControl>
)
}
function renderSortButton() {
// Renders a Button for sorting raids and a Tooltip for explaining what it does.
return (
<Tooltip
content={
sort === Sort.ASCENDING
? 'Lower difficulty battles first'
: 'Higher difficulty battles first'
}
>
<Button
blended={true}
buttonSize="small"
leftAccessoryIcon={<ArrowIcon />}
leftAccessoryClassName={sort === Sort.DESCENDING ? 'Flipped' : ''}
onClick={reverseSort}
/>
</Tooltip>
)
}
function renderTriggerContent() {
// Renders the content for the Popover trigger.
if (currentRaid) {
const element = (
<>
{!props.minimal && (
<div className="Info">
<span className="Group">{currentRaid.group.name[locale]}</span>
<span className="Separator">/</span>
<span className={classNames({ Raid: true }, linkClass)}>
{currentRaid.name[locale]}
</span>
</div>
)}
{currentRaid.group.extra && !props.minimal && (
<i className="ExtraIndicator">EX</i>
)}
</>
)
return {
element,
rawValue: currentRaid.id,
}
}
return undefined
}
function renderSearchInput() {
// Renders the search input for the raid combobox
return (
<div className="Bound Joined">
<CommandInput
className="Input"
placeholder={t('search.placeholders.raid')}
value={query}
onValueChange={setQuery}
/>
<div
className={classNames({
Button: true,
Clear: true,
Visible: query.length > 0,
})}
onClick={clearSearch}
>
<CrossIcon />
</div>
</div>
)
}
return ( return (
<Popover <Popover
className="Flush" className="Flush"
@ -238,103 +354,20 @@ const RaidCombobox = (props: Props) => {
onOpenChange={toggleOpen} onOpenChange={toggleOpen}
placeholder={t('raids.placeholder')} placeholder={t('raids.placeholder')}
trigger={{ className: 'Raid' }} trigger={{ className: 'Raid' }}
value={ value={renderTriggerContent()}
currentRaid
? {
element: (
<>
<div className="Info">
<span className="Group">
{currentRaid?.group.name[locale]}
</span>
<span className="Separator">/</span>
<span className={classNames({ Raid: true }, linkClass)}>
{currentRaid?.name[locale]}
</span>
</div>
{currentRaid.group.extra ? (
<i className="ExtraIndicator">EX</i>
) : (
''
)}
</>
),
rawValue: currentRaid?.id,
}
: undefined
}
> >
<Command className="Raid Combobox"> <Command className="Raid Combobox">
<div className="Header"> <div className="Header">
<div className="Bound Joined"> {renderSearchInput()}
<CommandInput {!query && (
className="Input"
placeholder={t('search.placeholders.raid')}
value={search}
onValueChange={setSearch}
/>
<div
className={classNames({
Button: true,
Visible: search.length > 0,
})}
onClick={clearSearch}
>
<CrossIcon />
</div>
</div>
{!search ? (
<div className="Controls"> <div className="Controls">
<SegmentedControl blended={true}> {renderSegmentedControl()}
<Segment {renderSortButton()}
groupName="raid_section"
name="events"
selected={currentSection === 2}
onClick={() => setCurrentSection(2)}
>
{t('raids.sections.events')}
</Segment>
<Segment
groupName="raid_section"
name="raids"
selected={currentSection === 1}
onClick={() => setCurrentSection(1)}
>
{t('raids.sections.raids')}
</Segment>
<Segment
groupName="raid_section"
name="solo"
selected={currentSection === 3}
onClick={() => setCurrentSection(3)}
>
{t('raids.sections.solo')}
</Segment>
</SegmentedControl>
<Tooltip
content={
sort === Sort.ASCENDING
? 'Lower difficulty battles first'
: 'Higher difficulty battles first'
}
>
<Button
blended={true}
buttonSize="small"
leftAccessoryIcon={<ArrowIcon />}
leftAccessoryClassName={
sort === Sort.DESCENDING ? 'Flipped' : ''
}
onClick={reverseSort}
/>
</Tooltip>
</div> </div>
) : (
''
)} )}
</div> </div>
<div <div
className={classNames({ Raids: true, Searching: search !== '' })} className={classNames({ Raids: true, Searching: query !== '' })}
ref={listRef} ref={listRef}
> >
{renderRaidSections()} {renderRaidSections()}
@ -344,4 +377,8 @@ const RaidCombobox = (props: Props) => {
) )
} }
RaidCombobox.defaultProps = {
minimal: false,
}
export default RaidCombobox export default RaidCombobox