From 02d92533dde28faf16d7ca84b6c89cba8a59bfb3 Mon Sep 17 00:00:00 2001 From: arquelion Date: Sun, 29 Sep 2024 20:42:50 -0700 Subject: [PATCH 1/4] checkpoint: CharacterSelection and CharacterMultiSelection refactored; selected teammates pinned to the top for single select; indicator for which slot is being selected WIP --- libs/gi/page-team/src/TeamSetting/index.tsx | 12 +- .../CharacterMultiSelectionModal.tsx | 519 ------------------ .../character/CharacterSelectionModal.tsx | 164 ++++-- libs/gi/ui/src/components/character/index.ts | 1 - 4 files changed, 132 insertions(+), 564 deletions(-) delete mode 100644 libs/gi/ui/src/components/character/CharacterMultiSelectionModal.tsx diff --git a/libs/gi/page-team/src/TeamSetting/index.tsx b/libs/gi/page-team/src/TeamSetting/index.tsx index e172522dd8..4506e8c5ea 100644 --- a/libs/gi/page-team/src/TeamSetting/index.tsx +++ b/libs/gi/page-team/src/TeamSetting/index.tsx @@ -6,7 +6,6 @@ import type { TeamData, dataContextObj } from '@genshin-optimizer/gi/ui' import { AdResponsive, CharIconSide, - CharacterMultiSelectionModal, CharacterName, CharacterSelectionModal, EnemyExpandCard, @@ -116,6 +115,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 +170,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) @@ -216,20 +213,21 @@ function TeamEditor({ <> c !== charKeyAtIndex} show={!showMultiSelect && charSelectIndex !== undefined} onHide={() => setCharSelectIndex(undefined)} + teamId={teamId} onSelect={onSingleSelect} /> - { setShowMultiSelect(false) }} - onMultiSelect={onMultiSelect} teamId={teamId} + multiSelect + onMultiSelect={onMultiSelect} /> 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..185a5e6bef 100644 --- a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx +++ b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx @@ -24,15 +24,13 @@ import { allElementKeys, allWeaponTypeKeys, } from '@genshin-optimizer/gi/consts' -import type { ICachedCharacter } from '@genshin-optimizer/gi/db' import { useCharMeta, useCharacter, useDBMeta, useDatabase, + useTeam, } 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' @@ -73,26 +71,24 @@ import { } from './CharacterSort' import { CharacterName } from './Trans' -type characterFilter = ( - characterKey: CharacterKey, - character: ICachedCharacter | undefined, - sheet: CharacterSheet -) => boolean - type CharacterSelectionModalProps = { show: boolean newFirst?: boolean onHide: () => void + teamId: string + multiSelect?: boolean onSelect?: (ckey: CharacterKey) => void - filter?: characterFilter + onMultiSelect?: (cKeys: (CharacterKey | '')[]) => void } const sortKeys = Object.keys(characterSortMap) export function CharacterSelectionModal({ show, onHide, + teamId, onSelect, - filter = () => true, + onMultiSelect, newFirst = false, + multiSelect = false, }: CharacterSelectionModalProps) { const { t } = useTranslation([ 'page_character', @@ -104,7 +100,41 @@ export function CharacterSelectionModal({ const database = useDatabase() const state = useDataEntryBase(database.displayCharacter) - const { gender } = useDBMeta() + 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() @@ -124,12 +154,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,16 +172,15 @@ export function CharacterSelectionModal({ ['new', 'favorite'] ) ) - ) + return cachedTeamCharKeys.filter((key) => key !== '').concat(filteredKeys) }, [ deferredState, newFirst, deferredDbDirty, deferredSearchTerm, database, + cachedTeamCharKeys, silly, - filter, - gender, ]) const weaponTotals = useMemo( @@ -182,11 +209,35 @@ export function CharacterSelectionModal({ const { weaponType, element, sortType, ascending } = state + // multi-select only + 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('') + multiSelect && onMultiSelect?.(teamCharKeys) onHide() }} containerProps={{ @@ -213,17 +264,19 @@ export function CharacterSelectionModal({ + onChange={(weaponType) => { database.displayCharacter.set({ weaponType }) - } + setCachedTeamCharKeys(teamCharKeys) + }} value={weaponType} totals={weaponTotals} size="small" /> + onChange={(element) => { database.displayCharacter.set({ element }) - } + setCachedTeamCharKeys(teamCharKeys) + }} value={element} totals={elementTotals} size="small" @@ -233,6 +286,7 @@ export function CharacterSelectionModal({ sx={{ ml: 'auto' }} onClick={() => { setSearchTerm('') + multiSelect && onMultiSelect?.(teamCharKeys) onHide() }} > @@ -244,9 +298,10 @@ export function CharacterSelectionModal({ ) => + onChange={(e: ChangeEvent) => { setSearchTerm(e.target.value) - } + setCachedTeamCharKeys(teamCharKeys) + }} label={t('characterName')} size="small" sx={{ height: '100%', mr: 'auto' }} @@ -257,13 +312,15 @@ export function CharacterSelectionModal({ + onChange={(sortType) => { database.displayCharacter.set({ sortType }) - } + setCachedTeamCharKeys(teamCharKeys) + }} ascending={ascending} - onChangeAsc={(ascending) => + onChangeAsc={(ascending) => { database.displayCharacter.set({ ascending }) - } + setCachedTeamCharKeys(teamCharKeys) + }} /> @@ -277,14 +334,24 @@ export function CharacterSelectionModal({ > {characterKeyList.map((characterKey) => ( - { - setSearchTerm('') - onHide() - onSelect?.(characterKey) - }} - /> + {multiSelect ? ( + onClick(characterKey)} + showTeamSlot + selectedIndex={teamCharKeys.indexOf(characterKey)} + /> + ) : ( + { + setSearchTerm('') + onHide() + onSelect?.(characterKey) + }} + selectedIndex={teamCharKeys.indexOf(characterKey)} + /> + )} ))} @@ -306,9 +373,13 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( function SelectionCard({ characterKey, onClick, + showTeamSlot = false, + selectedIndex = 0, }: { characterKey: CharacterKey onClick: () => void + showTeamSlot?: boolean + selectedIndex?: number }) { const { gender } = useDBMeta() const character = useCharacter(characterKey) @@ -321,6 +392,8 @@ function SelectionCard({ const { level = 1, ascension = 0, constellation = 0 } = character ?? {} const banner = characterAsset(characterKey, 'banner', gender) const rarity = getCharStat(characterKey).rarity + + const isSelected = selectedIndex !== -1 return ( {favorite ? : } + {showTeamSlot && isSelected && ( + + + {selectedIndex + 1} + + + )} Date: Tue, 1 Oct 2024 15:19:51 -0700 Subject: [PATCH 2/4] checkpoint: animation added, char select breaks character page due to no teamid --- libs/gi/page-team/src/TeamSetting/index.tsx | 1 + .../character/CharacterSelectionModal.tsx | 41 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/libs/gi/page-team/src/TeamSetting/index.tsx b/libs/gi/page-team/src/TeamSetting/index.tsx index 4506e8c5ea..8ea9618745 100644 --- a/libs/gi/page-team/src/TeamSetting/index.tsx +++ b/libs/gi/page-team/src/TeamSetting/index.tsx @@ -216,6 +216,7 @@ function TeamEditor({ show={!showMultiSelect && charSelectIndex !== undefined} onHide={() => setCharSelectIndex(undefined)} teamId={teamId} + selectedIndex={charSelectIndex} onSelect={onSingleSelect} /> diff --git a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx index 185a5e6bef..59dcec00c4 100644 --- a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx +++ b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx @@ -47,6 +47,7 @@ import { TextField, Tooltip, Typography, + keyframes, styled, tooltipClasses, } from '@mui/material' @@ -75,8 +76,9 @@ type CharacterSelectionModalProps = { show: boolean newFirst?: boolean onHide: () => void - teamId: string + teamId?: string multiSelect?: boolean + selectedIndex?: number onSelect?: (ckey: CharacterKey) => void onMultiSelect?: (cKeys: (CharacterKey | '')[]) => void } @@ -84,11 +86,12 @@ const sortKeys = Object.keys(characterSortMap) export function CharacterSelectionModal({ show, onHide, - teamId, onSelect, onMultiSelect, + teamId = '', newFirst = false, multiSelect = false, + selectedIndex = -1, }: CharacterSelectionModalProps) { const { t } = useTranslation([ 'page_character', @@ -100,7 +103,10 @@ export function CharacterSelectionModal({ const database = useDatabase() const state = useDataEntryBase(database.displayCharacter) - const { loadoutData } = useTeam(teamId)! + const team = useTeam(teamId)! + const loadoutData = useMemo(() => { + return team?.loadoutData ?? [undefined, undefined, undefined, undefined] + }, [team]) const [teamCharKeys, setTeamCharKeys] = useState(['', '', '', ''] as ( | CharacterKey | '' @@ -338,8 +344,7 @@ export function CharacterSelectionModal({ onClick(characterKey)} - showTeamSlot - selectedIndex={teamCharKeys.indexOf(characterKey)} + teamSlotIndex={teamCharKeys.indexOf(characterKey)} /> ) : ( )} @@ -373,13 +379,13 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( function SelectionCard({ characterKey, onClick, - showTeamSlot = false, - selectedIndex = 0, + selectedIndex = -1, + teamSlotIndex = 0, }: { characterKey: CharacterKey onClick: () => void - showTeamSlot?: boolean selectedIndex?: number + teamSlotIndex?: number }) { const { gender } = useDBMeta() const character = useCharacter(characterKey) @@ -393,7 +399,15 @@ function SelectionCard({ const banner = characterAsset(characterKey, 'banner', gender) const rarity = getCharStat(characterKey).rarity - const isSelected = selectedIndex !== -1 + const isInTeam = teamSlotIndex !== -1 + const isMulti = selectedIndex === -1 + + const flash = keyframes` + 0% {outline-color: #f7bd10} + 33% {outline-color: #1b263b} + 66% {outline-color: #f7bd10} + 100% {outline-color: #f7bd10} + ` return ( {favorite ? : } - {showTeamSlot && isSelected && ( + {isMulti && isInTeam && ( - {selectedIndex + 1} + {teamSlotIndex + 1} )} From c2630130e90b5fddf27572252f8a4e95f6eaa707 Mon Sep 17 00:00:00 2001 From: arquelion Date: Tue, 1 Oct 2024 15:45:39 -0700 Subject: [PATCH 3/4] CharacterSelectionModal works from all locations but is really laggy from the character page locally --- libs/gi/page-team/src/TeamSetting/index.tsx | 4 ++-- .../character/CharacterSelectionModal.tsx | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/libs/gi/page-team/src/TeamSetting/index.tsx b/libs/gi/page-team/src/TeamSetting/index.tsx index 8ea9618745..c2d450c45f 100644 --- a/libs/gi/page-team/src/TeamSetting/index.tsx +++ b/libs/gi/page-team/src/TeamSetting/index.tsx @@ -215,7 +215,7 @@ function TeamEditor({ setCharSelectIndex(undefined)} - teamId={teamId} + loadoutData={loadoutData} selectedIndex={charSelectIndex} onSelect={onSingleSelect} /> @@ -226,7 +226,7 @@ function TeamEditor({ onHide={() => { setShowMultiSelect(false) }} - teamId={teamId} + loadoutData={loadoutData} multiSelect onMultiSelect={onMultiSelect} /> diff --git a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx index 59dcec00c4..54ee6c735b 100644 --- a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx +++ b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx @@ -24,6 +24,7 @@ import { allElementKeys, allWeaponTypeKeys, } from '@genshin-optimizer/gi/consts' +import type { LoadoutDatum } from '@genshin-optimizer/gi/db' import { useCharMeta, useCharacter, @@ -76,7 +77,7 @@ type CharacterSelectionModalProps = { show: boolean newFirst?: boolean onHide: () => void - teamId?: string + loadoutData?: (LoadoutDatum | undefined)[] multiSelect?: boolean selectedIndex?: number onSelect?: (ckey: CharacterKey) => void @@ -88,7 +89,7 @@ export function CharacterSelectionModal({ onHide, onSelect, onMultiSelect, - teamId = '', + loadoutData = [undefined, undefined, undefined, undefined], newFirst = false, multiSelect = false, selectedIndex = -1, @@ -103,10 +104,6 @@ export function CharacterSelectionModal({ const database = useDatabase() const state = useDataEntryBase(database.displayCharacter) - const team = useTeam(teamId)! - const loadoutData = useMemo(() => { - return team?.loadoutData ?? [undefined, undefined, undefined, undefined] - }, [team]) const [teamCharKeys, setTeamCharKeys] = useState(['', '', '', ''] as ( | CharacterKey | '' @@ -344,6 +341,7 @@ export function CharacterSelectionModal({ onClick(characterKey)} + isMulti teamSlotIndex={teamCharKeys.indexOf(characterKey)} /> ) : ( @@ -379,11 +377,13 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( function SelectionCard({ characterKey, onClick, + isMulti = false, selectedIndex = -1, teamSlotIndex = 0, }: { characterKey: CharacterKey onClick: () => void + isMulti?: boolean selectedIndex?: number teamSlotIndex?: number }) { @@ -400,7 +400,6 @@ function SelectionCard({ const rarity = getCharStat(characterKey).rarity const isInTeam = teamSlotIndex !== -1 - const isMulti = selectedIndex === -1 const flash = keyframes` 0% {outline-color: #f7bd10} From 974d47c595036156e84d7e560e9aa78296d44791 Mon Sep 17 00:00:00 2001 From: arquelion Date: Sun, 6 Oct 2024 14:18:18 -0700 Subject: [PATCH 4/4] 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 --- libs/gi/page-characters/src/index.tsx | 4 +- libs/gi/page-team/src/TeamSetting/index.tsx | 14 +- .../character/CharacterSelectionModal.tsx | 733 ++++++++++++------ 3 files changed, 490 insertions(+), 261 deletions(-) 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 c2d450c45f..48609ab1e5 100644 --- a/libs/gi/page-team/src/TeamSetting/index.tsx +++ b/libs/gi/page-team/src/TeamSetting/index.tsx @@ -7,7 +7,8 @@ import { AdResponsive, CharIconSide, CharacterName, - CharacterSelectionModal, + CharacterMultiSelectionModal, + CharacterSingleSelectionModal, EnemyExpandCard, TeamDelModal, TeamInfoAlert, @@ -212,23 +213,22 @@ function TeamEditor({ return ( <> - setCharSelectIndex(undefined)} - loadoutData={loadoutData} - selectedIndex={charSelectIndex} onSelect={onSingleSelect} + selectedIndex={charSelectIndex} + loadoutData={loadoutData} /> - { setShowMultiSelect(false) }} + onSelect={onMultiSelect} loadoutData={loadoutData} - multiSelect - onMultiSelect={onMultiSelect} /> diff --git a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx index 54ee6c735b..d5ebef2bc7 100644 --- a/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx +++ b/libs/gi/ui/src/components/character/CharacterSelectionModal.tsx @@ -18,7 +18,7 @@ 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, @@ -30,7 +30,6 @@ import { 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' @@ -53,7 +52,7 @@ import { tooltipClasses, } from '@mui/material' import type { ChangeEvent } from 'react' -import { +import React, { useContext, useDeferredValue, useEffect, @@ -73,33 +72,156 @@ import { } from './CharacterSort' import { CharacterName } from './Trans' -type CharacterSelectionModalProps = { +export function CharacterSingleSelectionModal({ + show, + onHide, + onSelect, + selectedIndex = -1, + loadoutData = [undefined, undefined, undefined, undefined], + newFirst = false +} : { show: boolean - newFirst?: boolean onHide: () => void - loadoutData?: (LoadoutDatum | undefined)[] - multiSelect?: boolean + onSelect: (cKey: CharacterKey) => void selectedIndex?: number - onSelect?: (ckey: CharacterKey) => void - onMultiSelect?: (cKeys: (CharacterKey | '')[]) => void + 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 onChangeFilter = (type: ElementKey[] | WeaponTypeKey[]) => { + database.displayCharacter.set(type) + } + + const onChangeSearch = (e: ChangeEvent) => { + setSearchTerm(e.target.value) + } + + const onChangeSort = (sortType: string) => { + database.displayCharacter.set(sortType) + } + + const onChangeAsc = (asc: boolean) => { + database.displayCharacter.set(asc) + } + + const onClose = () => { + setSearchTerm('') + onHide() + } + + const filterSearchSortProps = { + searchTerm: searchTerm, + onChangeFilter: onChangeFilter, + onChangeSearch: onChangeSearch, + onChangeSort: onChangeSort, + onChangeAsc: onChangeAsc, + } + + return ( + + + + {characterKeyList.map((characterKey) => ( + + + { + setSearchTerm('') + onHide() + onSelect(characterKey) + }} + /> + + + ))} + + + + ) } -const sortKeys = Object.keys(characterSortMap) -export function CharacterSelectionModal({ + +export function CharacterMultiSelectionModal({ show, onHide, onSelect, - onMultiSelect, loadoutData = [undefined, undefined, undefined, undefined], - newFirst = false, - multiSelect = false, - selectedIndex = -1, -}: CharacterSelectionModalProps) { - const { t } = useTranslation([ - 'page_character', - // Always load these 2 so character names are loaded for searching/sorting - 'sillyWisher_charNames', - 'charNames_gen', - ]) + newFirst = false +} : { + 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) @@ -186,33 +308,6 @@ export function CharacterSelectionModal({ 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 - - // multi-select only const onClick = (key: CharacterKey) => { const keySlotIndex = teamCharKeys.indexOf(key) const firstOpenIndex = teamCharKeys.indexOf('') @@ -235,14 +330,135 @@ export function CharacterSelectionModal({ } } + const onChangeFilter = (type: ElementKey[] | WeaponTypeKey[]) => { + database.displayCharacter.set(type) + setCachedTeamCharKeys(teamCharKeys) + } + + const onChangeSearch = (e: ChangeEvent) => { + setSearchTerm(e.target.value) + setCachedTeamCharKeys(teamCharKeys) + } + + const onChangeSort = (sortType: string) => { + database.displayCharacter.set(sortType) + setCachedTeamCharKeys(teamCharKeys) + } + + const onChangeAsc = (asc: boolean) => { + database.displayCharacter.set(asc) + setCachedTeamCharKeys(teamCharKeys) + } + + const onClose = () => { + setSearchTerm('') + onSelect(teamCharKeys) + onHide() + } + + const filterSearchSortProps = { + searchTerm: searchTerm, + onChangeFilter: onChangeFilter, + onChangeSearch: onChangeSearch, + onChangeSort: onChangeSort, + onChangeAsc: onChangeAsc, + } + + return ( + + + + {characterKeyList.map((characterKey) => ( + + + onClick(characterKey)} + /> + + + ))} + + + + ) +} + +type FilterSearchSortProps = { + searchTerm: string + onChangeFilter: (filters: ElementKey[] | WeaponTypeKey[]) => void + onChangeSearch: (e: ChangeEvent) => void + onChangeSort: (sortType: string) => 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 (charactersToShow.includes(ck)) ct[wtk].current++ + }) + ), + [charactersToShow] + ) + + const elementTotals = useMemo( + () => + catTotal(allElementKeys, (ct) => + allCharacterKeys.forEach((ck) => { + const ele = getCharEle(ck) + ct[ele].total++ + if (charactersToShow.includes(ck)) ct[ele].current++ + }) + ), + [charactersToShow] + ) + + const { weaponType, element, sortType, ascending } = state + return ( { - setSearchTerm('') - multiSelect && onMultiSelect?.(teamCharKeys) - onHide() - }} + onClose={onClose} containerProps={{ sx: { height: '100vh', @@ -267,19 +483,13 @@ export function CharacterSelectionModal({ { - database.displayCharacter.set({ weaponType }) - setCachedTeamCharKeys(teamCharKeys) - }} + onChange={(weaponType) => filterSearchSortProps.onChangeFilter({ weaponType })} value={weaponType} totals={weaponTotals} size="small" /> { - database.displayCharacter.set({ element }) - setCachedTeamCharKeys(teamCharKeys) - }} + onChange={(element) => filterSearchSortProps.onChangeFilter({ element })} value={element} totals={elementTotals} size="small" @@ -287,24 +497,16 @@ export function CharacterSelectionModal({ { - setSearchTerm('') - multiSelect && onMultiSelect?.(teamCharKeys) - onHide() - }} + onClick={() => onClose()} > - ) => { - setSearchTerm(e.target.value) - setCachedTeamCharKeys(teamCharKeys) - }} + value={filterSearchSortProps.searchTerm} + onChange={(e: ChangeEvent) => filterSearchSortProps.onChangeSearch(e)} label={t('characterName')} size="small" sx={{ height: '100%', mr: 'auto' }} @@ -315,51 +517,15 @@ export function CharacterSelectionModal({ { - database.displayCharacter.set({ sortType }) - setCachedTeamCharKeys(teamCharKeys) - }} + onChange={(sortType) => filterSearchSortProps.onChangeSort({ sortType })} ascending={ascending} - onChangeAsc={(ascending) => { - database.displayCharacter.set({ ascending }) - setCachedTeamCharKeys(teamCharKeys) - }} + onChangeAsc={(ascending) => filterSearchSortProps.onChangeAsc({ ascending })} /> - - - {characterKeyList.map((characterKey) => ( - - {multiSelect ? ( - onClick(characterKey)} - isMulti - teamSlotIndex={teamCharKeys.indexOf(characterKey)} - /> - ) : ( - { - setSearchTerm('') - onHide() - onSelect?.(characterKey) - }} - selectedIndex={selectedIndex} - teamSlotIndex={teamCharKeys.indexOf(characterKey)} - /> - )} - - ))} - - + {children} @@ -374,39 +540,29 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => ( }, }) -function SelectionCard({ +function SingleSelectCardWrapper({ characterKey, - onClick, - isMulti = false, + children, selectedIndex = -1, - teamSlotIndex = 0, -}: { + teamSlotIndex = -1, +} : { characterKey: CharacterKey - onClick: () => void - isMulti?: boolean - selectedIndex?: number - teamSlotIndex?: number + 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 isInTeam = teamSlotIndex !== -1 - const flash = keyframes` - 0% {outline-color: #f7bd10} - 33% {outline-color: #1b263b} - 66% {outline-color: #f7bd10} - 100% {outline-color: #f7bd10} + 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 }) }} > - { - onClose() - database.charMeta.set(characterKey, { favorite: !favorite }) - }} - > - {favorite ? : } - - {isMulti && isInTeam && ( - - - {teamSlotIndex + 1} - - - )} - - : } + + {children} + + + ) +} + +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 }) + }} + > + {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 + + )} + + - + ) }