From fe3a358b645416cfb32976b73c602522ee1316a8 Mon Sep 17 00:00:00 2001 From: arquelion <47800040+arquelion@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:51:57 -0700 Subject: [PATCH] CharacterSelectionModal refactoring and followup changes for single selection (#2473) * checkpoint: CharacterSelection and CharacterMultiSelection refactored; selected teammates pinned to the top for single select; indicator for which slot is being selected WIP * checkpoint: animation added, char select breaks character page due to no teamid * CharacterSelectionModal works from all locations but is really laggy from the character page locally * Refactor Take 2: CharacterSelectionModal code split into a single select and multi select modal with shared code in a base component; Moved some of SelectionCard code out into to separate wrappers for single and multi select containing the tooltip, fav toggles, and outlines/team slot number chips * fix typecheck errors * added some comments on refactored components --- libs/gi/page-characters/src/index.tsx | 4 +- libs/gi/page-team/src/TeamSetting/index.tsx | 15 +- .../CharacterMultiSelectionModal.tsx | 519 ------------- .../character/CharacterSelectionModal.tsx | 703 +++++++++++++----- libs/gi/ui/src/components/character/index.ts | 1 - 5 files changed, 528 insertions(+), 714 deletions(-) delete mode 100644 libs/gi/ui/src/components/character/CharacterMultiSelectionModal.tsx diff --git a/libs/gi/page-characters/src/index.tsx b/libs/gi/page-characters/src/index.tsx index fd56ee1fd3..4bf552c707 100644 --- a/libs/gi/page-characters/src/index.tsx +++ b/libs/gi/page-characters/src/index.tsx @@ -29,7 +29,7 @@ import { CharacterCard, CharacterEditor, CharacterRarityToggle, - CharacterSelectionModal, + CharacterSingleSelectionModal, ElementToggle, SillyContext, WeaponToggle, @@ -222,7 +222,7 @@ export default function PageCharacter() { /> )} - setnewCharacter(false)} diff --git a/libs/gi/page-team/src/TeamSetting/index.tsx b/libs/gi/page-team/src/TeamSetting/index.tsx index e172522dd8..7e4f09d02f 100644 --- a/libs/gi/page-team/src/TeamSetting/index.tsx +++ b/libs/gi/page-team/src/TeamSetting/index.tsx @@ -8,7 +8,7 @@ import { CharIconSide, CharacterMultiSelectionModal, CharacterName, - CharacterSelectionModal, + CharacterSingleSelectionModal, EnemyExpandCard, TeamDelModal, TeamInfoAlert, @@ -116,6 +116,7 @@ function TeamEditor({ const database = useDatabase() const team = database.teams.get(teamId)! const { loadoutData } = team + const onSelect = (cKey: CharacterKey, selectedIndex: number) => { // Make sure character exists database.chars.getWithInitWeapon(cKey) @@ -170,9 +171,6 @@ function TeamEditor({ const [charSelectIndex, setCharSelectIndex] = useState( undefined as number | undefined ) - const charKeyAtIndex = database.teamChars.get( - loadoutData[charSelectIndex as number]?.teamCharId - )?.key const onSingleSelect = (cKey: CharacterKey) => { if (charSelectIndex === undefined) return onSelect(cKey, charSelectIndex) @@ -215,11 +213,12 @@ function TeamEditor({ return ( <> - c !== charKeyAtIndex} + setCharSelectIndex(undefined)} onSelect={onSingleSelect} + selectedIndex={charSelectIndex} + loadoutData={loadoutData} /> @@ -228,8 +227,8 @@ function TeamEditor({ onHide={() => { setShowMultiSelect(false) }} - onMultiSelect={onMultiSelect} - teamId={teamId} + onSelect={onMultiSelect} + loadoutData={loadoutData} /> diff --git a/libs/gi/ui/src/components/character/CharacterMultiSelectionModal.tsx b/libs/gi/ui/src/components/character/CharacterMultiSelectionModal.tsx deleted file mode 100644 index 67be871a5f..0000000000 --- a/libs/gi/ui/src/components/character/CharacterMultiSelectionModal.tsx +++ /dev/null @@ -1,519 +0,0 @@ -'use client' -import { useDataEntryBase } from '@genshin-optimizer/common/database-ui' -import { - useBoolState, - useForceUpdate, -} from '@genshin-optimizer/common/react-util' -import { - CardThemed, - ModalWrapper, - NextImage, - SortByButton, - SqBadge, - StarsDisplay, -} from '@genshin-optimizer/common/ui' -import { - catTotal, - filterFunction, - sortFunction, -} from '@genshin-optimizer/common/util' -import { characterAsset } from '@genshin-optimizer/gi/assets' -import type { CharacterKey } from '@genshin-optimizer/gi/consts' -import { - allCharacterKeys, - allElementKeys, - allWeaponTypeKeys, -} from '@genshin-optimizer/gi/consts' -import { - useCharMeta, - useCharacter, - useDBMeta, - useDatabase, - useTeam, -} from '@genshin-optimizer/gi/db-ui' -import { getCharEle, getCharStat } from '@genshin-optimizer/gi/stats' -import { ascensionMaxLevel } from '@genshin-optimizer/gi/util' -import CloseIcon from '@mui/icons-material/Close' -import FavoriteIcon from '@mui/icons-material/Favorite' -import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder' -import type { TooltipProps } from '@mui/material' -import { - Box, - CardActionArea, - CardContent, - Divider, - Grid, - IconButton, - TextField, - Tooltip, - Typography, - styled, - tooltipClasses, -} from '@mui/material' -import type { ChangeEvent } from 'react' -import { - useContext, - useDeferredValue, - useEffect, - useMemo, - useState, -} from 'react' -import { useTranslation } from 'react-i18next' -import { DataContext, SillyContext } from '../../context' -import { iconAsset } from '../../util/iconAsset' -import { ElementToggle, WeaponToggle } from '../toggles' -import { CharacterCard } from './CharacterCard' -import type { CharacterSortKey } from './CharacterSort' -import { - characterFilterConfigs, - characterSortConfigs, - characterSortMap, -} from './CharacterSort' -import { CharacterName } from './Trans' - -type CharacterMultiSelectionModalProps = { - show: boolean - newFirst?: boolean - onHide: () => void - onMultiSelect?: (cKeys: (CharacterKey | '')[]) => void - teamId: string -} -const sortKeys = Object.keys(characterSortMap) -export function CharacterMultiSelectionModal({ - show, - onHide, - onMultiSelect, - teamId, - newFirst = false, -}: CharacterMultiSelectionModalProps) { - const { t } = useTranslation([ - 'page_character', - // Always load these 2 so character names are loaded for searching/sorting - 'sillyWisher_charNames', - 'charNames_gen', - ]) - const { silly } = useContext(SillyContext) - const database = useDatabase() - const state = useDataEntryBase(database.displayCharacter) - - const { loadoutData } = useTeam(teamId)! - const [teamCharKeys, setTeamCharKeys] = useState(['', '', '', ''] as ( - | CharacterKey - | '' - )[]) - // update teamCharKeys when loadoutData changes - useEffect( - () => - setTeamCharKeys( - loadoutData.map( - (loadoutDatum) => - database.teamChars.get(loadoutDatum?.teamCharId)?.key ?? '' - ) - ), - [database, loadoutData, setTeamCharKeys] - ) - - // used for generating characterKeyList below, only updated when filter/sort/search is applied to prevent characters - // from moving around as soon as they as selected/deselected for the team - const [cachedTeamCharKeys, setCachedTeamCharKeys] = useState([ - '', - '', - '', - '', - ] as (CharacterKey | '')[]) - useEffect( - () => - setCachedTeamCharKeys( - loadoutData.map( - (loadoutDatum) => - database.teamChars.get(loadoutDatum?.teamCharId)?.key ?? '' - ) - ), - [database, loadoutData, setCachedTeamCharKeys] - ) - - const [dbDirty, forceUpdate] = useForceUpdate() - - // character favorite updater - useEffect( - () => database.charMeta.followAny(() => forceUpdate()), - [forceUpdate, database] - ) - - const [searchTerm, setSearchTerm] = useState('') - const deferredSearchTerm = useDeferredValue(searchTerm) - const deferredState = useDeferredValue(state) - const deferredDbDirty = useDeferredValue(dbDirty) - const characterKeyList = useMemo(() => { - const { element, weaponType, sortType, ascending } = deferredState - const sortByKeys = [ - ...(newFirst ? ['new'] : []), - ...(characterSortMap[sortType] ?? []), - ] as CharacterSortKey[] - const filteredKeys = - deferredDbDirty && - allCharacterKeys - .filter((key) => cachedTeamCharKeys.indexOf(key) === -1) - .filter( - filterFunction( - { element, weaponType, name: deferredSearchTerm }, - characterFilterConfigs(database, silly) - ) - ) - .sort( - sortFunction( - sortByKeys, - ascending, - characterSortConfigs(database, silly), - ['new', 'favorite'] - ) - ) - return cachedTeamCharKeys.filter((key) => key !== '').concat(filteredKeys) - }, [ - deferredState, - newFirst, - deferredDbDirty, - deferredSearchTerm, - database, - cachedTeamCharKeys, - silly, - ]) - - const weaponTotals = useMemo( - () => - catTotal(allWeaponTypeKeys, (ct) => - allCharacterKeys.forEach((ck) => { - const wtk = getCharStat(ck).weaponType - ct[wtk].total++ - if (characterKeyList.includes(ck)) ct[wtk].current++ - }) - ), - [characterKeyList] - ) - - const elementTotals = useMemo( - () => - catTotal(allElementKeys, (ct) => - allCharacterKeys.forEach((ck) => { - const ele = getCharEle(ck) - ct[ele].total++ - if (characterKeyList.includes(ck)) ct[ele].current++ - }) - ), - [characterKeyList] - ) - - const { weaponType, element, sortType, ascending } = state - - const onClick = (key: CharacterKey) => { - const keySlotIndex = teamCharKeys.indexOf(key) - const firstOpenIndex = teamCharKeys.indexOf('') - if (keySlotIndex === -1) { - // Selected character was previously unselected, add to the list of currently selected keys if team is not full - if (firstOpenIndex === -1) return - setTeamCharKeys([ - ...teamCharKeys.slice(0, firstOpenIndex), - key, - ...teamCharKeys.slice(firstOpenIndex + 1), - ]) - } else { - // Selected character was previously selected, so replace the slot with - // '' to indicate the slot is currently empty - setTeamCharKeys([ - ...teamCharKeys.slice(0, keySlotIndex), - '', - ...teamCharKeys.slice(keySlotIndex + 1), - ]) - } - } - - return ( - { - setSearchTerm('') - onMultiSelect?.(teamCharKeys) - onHide() - }} - containerProps={{ - sx: { - height: '100vh', - p: { xs: 1 }, - }, - }} - > - - - - - { - database.displayCharacter.set({ weaponType }) - setCachedTeamCharKeys(teamCharKeys) - }} - value={weaponType} - totals={weaponTotals} - size="small" - /> - { - database.displayCharacter.set({ element }) - setCachedTeamCharKeys(teamCharKeys) - }} - value={element} - totals={elementTotals} - size="small" - /> - - { - setSearchTerm('') - onMultiSelect?.(teamCharKeys) - onHide() - }} - > - - - - - - ) => { - setSearchTerm(e.target.value) - setCachedTeamCharKeys(teamCharKeys) - }} - label={t('characterName')} - size="small" - sx={{ height: '100%', mr: 'auto' }} - InputProps={{ - sx: { height: '100%' }, - }} - /> - { - database.displayCharacter.set({ sortType }) - setCachedTeamCharKeys(teamCharKeys) - }} - ascending={ascending} - onChangeAsc={(ascending) => { - database.displayCharacter.set({ ascending }) - setCachedTeamCharKeys(teamCharKeys) - }} - /> - - - - - - - {characterKeyList.map((characterKey) => ( - - onClick(characterKey)} - selectedIndex={teamCharKeys.indexOf(characterKey)} - /> - - ))} - - - - - - ) -} - -const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( - -))({ - [`& .${tooltipClasses.tooltip}`]: { - padding: 0, - }, -}) - -function SelectionCard({ - characterKey, - onClick, - selectedIndex, -}: { - characterKey: CharacterKey - onClick: () => void - selectedIndex: number -}) { - const { gender } = useDBMeta() - const character = useCharacter(characterKey) - const { favorite } = useCharMeta(characterKey) - const database = useDatabase() - const { silly } = useContext(SillyContext) - - const [open, onOpen, onClose] = useBoolState() - - const { level = 1, ascension = 0, constellation = 0 } = character ?? {} - const banner = characterAsset(characterKey, 'banner', gender) - const rarity = getCharStat(characterKey).rarity - - const isSelected = selectedIndex !== -1 - return ( - - - - } - > - - - { - onClose() - database.charMeta.set(characterKey, { favorite: !favorite }) - }} - > - {favorite ? : } - - {isSelected && ( - - - {selectedIndex + 1} - - - )} - - - - - - - - - - - - {character ? ( - - - - Lv. {level} - - - /{ascensionMaxLevel[ascension]} - - - C{constellation} - - ) : ( - - NEW - - )} - - - - - - - - ) -} diff --git a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx index 4d5c017525..c895e2b8a4 100644 --- a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx +++ b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx @@ -18,21 +18,23 @@ import { sortFunction, } from '@genshin-optimizer/common/util' import { characterAsset } from '@genshin-optimizer/gi/assets' -import type { CharacterKey } from '@genshin-optimizer/gi/consts' +import type { + CharacterKey, + ElementKey, + WeaponTypeKey, +} from '@genshin-optimizer/gi/consts' import { allCharacterKeys, allElementKeys, allWeaponTypeKeys, } from '@genshin-optimizer/gi/consts' -import type { ICachedCharacter } from '@genshin-optimizer/gi/db' +import type { LoadoutDatum } from '@genshin-optimizer/gi/db' import { useCharMeta, useCharacter, useDBMeta, useDatabase, } from '@genshin-optimizer/gi/db-ui' -import type { CharacterSheet } from '@genshin-optimizer/gi/sheets' -import { getCharSheet } from '@genshin-optimizer/gi/sheets' import { getCharEle, getCharStat } from '@genshin-optimizer/gi/stats' import { ascensionMaxLevel } from '@genshin-optimizer/gi/util' import CloseIcon from '@mui/icons-material/Close' @@ -49,11 +51,12 @@ import { TextField, Tooltip, Typography, + keyframes, styled, tooltipClasses, } from '@mui/material' import type { ChangeEvent } from 'react' -import { +import React, { useContext, useDeferredValue, useEffect, @@ -73,38 +76,185 @@ import { } from './CharacterSort' import { CharacterName } from './Trans' -type characterFilter = ( - characterKey: CharacterKey, - character: ICachedCharacter | undefined, - sheet: CharacterSheet -) => boolean - -type CharacterSelectionModalProps = { +export function CharacterSingleSelectionModal({ + show, + onHide, + onSelect, + selectedIndex = -1, + loadoutData = [undefined, undefined, undefined, undefined], + newFirst = false, +}: { show: boolean - newFirst?: boolean onHide: () => void - onSelect?: (ckey: CharacterKey) => void - filter?: characterFilter + onSelect: (cKey: CharacterKey) => void + selectedIndex?: number + loadoutData?: (LoadoutDatum | undefined)[] + newFirst?: boolean +}) { + const { silly } = useContext(SillyContext) + const database = useDatabase() + const state = useDataEntryBase(database.displayCharacter) + + const teamCharKeys = loadoutData.map( + (loadoutDatum) => + database.teamChars.get(loadoutDatum?.teamCharId)?.key ?? '' + ) + + const [dbDirty, forceUpdate] = useForceUpdate() + + // character favorite updater + useEffect( + () => database.charMeta.followAny(() => forceUpdate()), + [forceUpdate, database] + ) + + const [searchTerm, setSearchTerm] = useState('') + const deferredSearchTerm = useDeferredValue(searchTerm) + const deferredState = useDeferredValue(state) + const deferredDbDirty = useDeferredValue(dbDirty) + const characterKeyList = useMemo(() => { + const { element, weaponType, sortType, ascending } = deferredState + const sortByKeys = [ + ...(newFirst ? ['new'] : []), + ...(characterSortMap[sortType] ?? []), + ] as CharacterSortKey[] + const filteredKeys = + deferredDbDirty && + allCharacterKeys + .filter((key) => teamCharKeys.indexOf(key) === -1) + .filter( + filterFunction( + { element, weaponType, name: deferredSearchTerm }, + characterFilterConfigs(database, silly) + ) + ) + .sort( + sortFunction( + sortByKeys, + ascending, + characterSortConfigs(database, silly), + ['new', 'favorite'] + ) + ) + return teamCharKeys.filter((key) => key !== '').concat(filteredKeys) + }, [ + deferredState, + newFirst, + deferredDbDirty, + deferredSearchTerm, + database, + teamCharKeys, + silly, + ]) + + const onClose = () => { + setSearchTerm('') + onHide() + } + + const filterSearchSortProps = { + searchTerm: searchTerm, + onChangeWeaponFilter: (weaponType: WeaponTypeKey[]) => { + database.displayCharacter.set({ weaponType }) + }, + onChangeElementFilter: (element: ElementKey[]) => { + database.displayCharacter.set({ element }) + }, + onChangeSearch: (e: ChangeEvent) => { + setSearchTerm(e.target.value) + }, + onChangeSort: (sortType: CharacterSortKey) => { + database.displayCharacter.set({ sortType }) + }, + onChangeAsc: (ascending: boolean) => { + database.displayCharacter.set({ ascending }) + }, + } + + return ( + + + + {characterKeyList.map((characterKey) => ( + + + { + setSearchTerm('') + onHide() + onSelect(characterKey) + }} + /> + + + ))} + + + + ) } -const sortKeys = Object.keys(characterSortMap) -export function CharacterSelectionModal({ + +export function CharacterMultiSelectionModal({ show, onHide, onSelect, - filter = () => true, + loadoutData = [undefined, undefined, undefined, undefined], newFirst = false, -}: CharacterSelectionModalProps) { - const { t } = useTranslation([ - 'page_character', - // Always load these 2 so character names are loaded for searching/sorting - 'sillyWisher_charNames', - 'charNames_gen', - ]) +}: { + show: boolean + onHide: () => void + onSelect: (cKeys: (CharacterKey | '')[]) => void + loadoutData: (LoadoutDatum | undefined)[] + newFirst?: boolean +}) { const { silly } = useContext(SillyContext) const database = useDatabase() const state = useDataEntryBase(database.displayCharacter) - const { gender } = useDBMeta() + const [teamCharKeys, setTeamCharKeys] = useState(['', '', '', ''] as ( + | CharacterKey + | '' + )[]) + // update teamCharKeys when loadoutData changes + useEffect( + () => + setTeamCharKeys( + loadoutData.map( + (loadoutDatum) => + database.teamChars.get(loadoutDatum?.teamCharId)?.key ?? '' + ) + ), + [database, loadoutData, setTeamCharKeys] + ) + + // used for generating characterKeyList below, only updated when filter/sort/search is applied to prevent characters + // from moving around as soon as they as selected/deselected for the team + const [cachedTeamCharKeys, setCachedTeamCharKeys] = useState([ + '', + '', + '', + '', + ] as (CharacterKey | '')[]) + useEffect( + () => + setCachedTeamCharKeys( + loadoutData.map( + (loadoutDatum) => + database.teamChars.get(loadoutDatum?.teamCharId)?.key ?? '' + ) + ), + [database, loadoutData, setCachedTeamCharKeys] + ) const [dbDirty, forceUpdate] = useForceUpdate() @@ -124,12 +274,10 @@ export function CharacterSelectionModal({ ...(newFirst ? ['new'] : []), ...(characterSortMap[sortType] ?? []), ] as CharacterSortKey[] - return ( + const filteredKeys = deferredDbDirty && allCharacterKeys - .filter((key) => - filter(key, database.chars.get(key), getCharSheet(key, gender)) - ) + .filter((key) => cachedTeamCharKeys.indexOf(key) === -1) .filter( filterFunction( { element, weaponType, name: deferredSearchTerm }, @@ -144,28 +292,141 @@ export function CharacterSelectionModal({ ['new', 'favorite'] ) ) - ) + return cachedTeamCharKeys.filter((key) => key !== '').concat(filteredKeys) }, [ deferredState, newFirst, deferredDbDirty, deferredSearchTerm, database, + cachedTeamCharKeys, silly, - filter, - gender, ]) + const onClick = (key: CharacterKey) => { + const keySlotIndex = teamCharKeys.indexOf(key) + const firstOpenIndex = teamCharKeys.indexOf('') + if (keySlotIndex === -1) { + // Selected character was previously unselected, add to the list of currently selected keys if team is not full + if (firstOpenIndex === -1) return + setTeamCharKeys([ + ...teamCharKeys.slice(0, firstOpenIndex), + key, + ...teamCharKeys.slice(firstOpenIndex + 1), + ]) + } else { + // Selected character was previously selected, so replace the slot with + // '' to indicate the slot is currently empty + setTeamCharKeys([ + ...teamCharKeys.slice(0, keySlotIndex), + '', + ...teamCharKeys.slice(keySlotIndex + 1), + ]) + } + } + + const onClose = () => { + setSearchTerm('') + onSelect(teamCharKeys) + onHide() + } + + const filterSearchSortProps = { + searchTerm: searchTerm, + onChangeWeaponFilter: (weaponType: WeaponTypeKey[]) => { + database.displayCharacter.set({ weaponType }) + setCachedTeamCharKeys(teamCharKeys) + }, + onChangeElementFilter: (element: ElementKey[]) => { + database.displayCharacter.set({ element }) + setCachedTeamCharKeys(teamCharKeys) + }, + onChangeSearch: (e: ChangeEvent) => { + setSearchTerm(e.target.value) + setCachedTeamCharKeys(teamCharKeys) + }, + onChangeSort: (sortType: CharacterSortKey) => { + database.displayCharacter.set({ sortType }) + setCachedTeamCharKeys(teamCharKeys) + }, + onChangeAsc: (ascending: boolean) => { + database.displayCharacter.set({ ascending }) + setCachedTeamCharKeys(teamCharKeys) + }, + } + + return ( + + + + {characterKeyList.map((characterKey) => ( + + + onClick(characterKey)} + /> + + + ))} + + + + ) +} + +type FilterSearchSortProps = { + searchTerm: string + onChangeWeaponFilter: (weaps: WeaponTypeKey[]) => void + onChangeElementFilter: (elements: ElementKey[]) => void + onChangeSearch: (e: ChangeEvent) => void + onChangeSort: (sortType: CharacterSortKey) => void + onChangeAsc: (asc: boolean) => void +} + +type CharacterSelectionModalBaseProps = { + show: boolean + charactersToShow: CharacterKey[] + filterSearchSortProps: FilterSearchSortProps + onClose: () => void + children: React.ReactNode +} +const sortKeys = Object.keys(characterSortMap) + +function CharacterSelectionModalBase({ + show, + charactersToShow, + filterSearchSortProps, + onClose, + children, +}: CharacterSelectionModalBaseProps) { + const { t } = useTranslation([ + 'page_character', + // Always load these 2 so character names are loaded for searching/sorting + 'sillyWisher_charNames', + 'charNames_gen', + ]) + const database = useDatabase() + const state = useDataEntryBase(database.displayCharacter) + const weaponTotals = useMemo( () => catTotal(allWeaponTypeKeys, (ct) => allCharacterKeys.forEach((ck) => { const wtk = getCharStat(ck).weaponType ct[wtk].total++ - if (characterKeyList.includes(ck)) ct[wtk].current++ + if (charactersToShow.includes(ck)) ct[wtk].current++ }) ), - [characterKeyList] + [charactersToShow] ) const elementTotals = useMemo( @@ -174,10 +435,10 @@ export function CharacterSelectionModal({ allCharacterKeys.forEach((ck) => { const ele = getCharEle(ck) ct[ele].total++ - if (characterKeyList.includes(ck)) ct[ele].current++ + if (charactersToShow.includes(ck)) ct[ele].current++ }) ), - [characterKeyList] + [charactersToShow] ) const { weaponType, element, sortType, ascending } = state @@ -185,10 +446,7 @@ export function CharacterSelectionModal({ return ( { - setSearchTerm('') - onHide() - }} + onClose={onClose} containerProps={{ sx: { height: '100vh', @@ -214,7 +472,7 @@ export function CharacterSelectionModal({ - database.displayCharacter.set({ weaponType }) + filterSearchSortProps.onChangeWeaponFilter(weaponType) } value={weaponType} totals={weaponTotals} @@ -222,30 +480,23 @@ export function CharacterSelectionModal({ /> - database.displayCharacter.set({ element }) + filterSearchSortProps.onChangeElementFilter(element) } value={element} totals={elementTotals} size="small" /> - { - setSearchTerm('') - onHide() - }} - > + onClose()}> - ) => - setSearchTerm(e.target.value) + filterSearchSortProps.onChangeSearch(e) } label={t('characterName')} size="small" @@ -258,37 +509,18 @@ export function CharacterSelectionModal({ sortKeys={sortKeys} value={sortType} onChange={(sortType) => - database.displayCharacter.set({ sortType }) + filterSearchSortProps.onChangeSort(sortType) } ascending={ascending} onChangeAsc={(ascending) => - database.displayCharacter.set({ ascending }) + filterSearchSortProps.onChangeAsc(ascending) } /> - - - {characterKeyList.map((characterKey) => ( - - { - setSearchTerm('') - onHide() - onSelect?.(characterKey) - }} - /> - - ))} - - + {children} @@ -303,24 +535,33 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( }, }) -function SelectionCard({ +// used for wrapping SelectionCards for the single selection variant - passing +// in the appropriate selectedIndex and teamSlotIndex controls whether outlining +// and flashing the selected outline is enabled, favorite icon and the character +// tooltip are always present +function SingleSelectCardWrapper({ characterKey, - onClick, + children, + selectedIndex = -1, + teamSlotIndex = -1, }: { characterKey: CharacterKey - onClick: () => void + children: React.ReactNode + selectedIndex: number + teamSlotIndex: number }) { - const { gender } = useDBMeta() - const character = useCharacter(characterKey) const { favorite } = useCharMeta(characterKey) const database = useDatabase() - const { silly } = useContext(SillyContext) const [open, onOpen, onClose] = useBoolState() - const { level = 1, ascension = 0, constellation = 0 } = character ?? {} - const banner = characterAsset(characterKey, 'banner', gender) - const rarity = getCharStat(characterKey).rarity + const flash = keyframes` + 0% {outline-color: #f7bd10} + 33% {outline-color: #1b263b} + 66% {outline-color: #f7bd10} + 100% {outline-color: #f7bd10} + ` + const isInTeam = teamSlotIndex !== -1 return ( } > - - + { + onClose() + database.charMeta.set(characterKey, { favorite: !favorite }) + }} + > + {favorite ? : } + + {children} + + + ) +} + +// used for wrapping SelectionCards for the multi select variant - components +// using this must provide the teamSlotIndex of the character this card is for +// (0-3 if the character is in the team being selected for, -1 otherwise) +function MultiSelectCardWrapper({ + characterKey, + teamSlotIndex, + children, +}: { + characterKey: CharacterKey + teamSlotIndex: number + children: React.ReactNode +}) { + const { favorite } = useCharMeta(characterKey) + const database = useDatabase() + + const [open, onOpen, onClose] = useBoolState() + + const isInTeam = teamSlotIndex !== -1 + return ( + + + + } + > + + { + onClose() + database.charMeta.set(characterKey, { favorite: !favorite }) }} > - { - onClose() - database.charMeta.set(characterKey, { favorite: !favorite }) - }} - > - {favorite ? : } - - - : } + + {isInTeam && ( + + - - - - - - - - + {teamSlotIndex + 1} + + + )} + {children} + + + ) +} + +function SelectionCard({ + characterKey, + onClick, +}: { + characterKey: CharacterKey + onClick: () => void +}) { + const { gender } = useDBMeta() + const character = useCharacter(characterKey) + const { silly } = useContext(SillyContext) + + const { level = 1, ascension = 0, constellation = 0 } = character ?? {} + const banner = characterAsset(characterKey, 'banner', gender) + const rarity = getCharStat(characterKey).rarity + + return ( + + + + + + + + + + + + {character ? ( + + + + Lv. {level} + + + /{ascensionMaxLevel[ascension]} - {character ? ( - - - - Lv. {level} - - - /{ascensionMaxLevel[ascension]} - - - C{constellation} - - ) : ( - - NEW - - )} - + C{constellation} - - + ) : ( + + NEW + + )} + + - + ) } diff --git a/libs/gi/ui/src/components/character/index.ts b/libs/gi/ui/src/components/character/index.ts index ee6190021e..f1e03bc9d6 100644 --- a/libs/gi/ui/src/components/character/index.ts +++ b/libs/gi/ui/src/components/character/index.ts @@ -3,7 +3,6 @@ export * from './card' export * from './CharacterCard' export * from './CharacterCardPico' export * from './CharacterMultiAutocomplete' -export * from './CharacterMultiSelectionModal' export * from './CharacterProfilePieces' export * from './CharacterSelectionModal' export * from './CharacterSort'