From 3ef9b17a36ad61aac4ad660a3ddf3fe05da4f6ef Mon Sep 17 00:00:00 2001 From: Evaldas Mikalauskas Date: Mon, 16 Dec 2024 00:44:35 +0200 Subject: [PATCH] Add builds page and build creation --- src/renderer/App.tsx | 42 ++-- src/renderer/components/BackButton.tsx | 23 ++ src/renderer/components/CompactItem.tsx | 25 +- src/renderer/components/Stash.tsx | 39 +++- src/renderer/contexts/buildsContext.tsx | 14 +- .../feature/builds/components/BuildCard.tsx | 65 ++++++ .../builds/components/BuildProgress.tsx | 133 +++++++++++ .../builds/components/ExportBuildButton.tsx | 44 ++++ .../feature/item/components/CraftsInto.tsx | 139 +++++++---- src/renderer/feature/item/components/Item.tsx | 22 +- src/renderer/index.tsx | 5 +- src/renderer/pages/BuildPage.tsx | 218 ++++++++++++++++++ src/renderer/pages/BuildsPage.tsx | 40 ++++ src/renderer/pages/CharacterPage.tsx | 42 ++-- src/renderer/pages/ItemPage.tsx | 71 ++++-- src/renderer/pages/ItemsPage.tsx | 25 +- src/renderer/util/crafting.ts | 54 +++-- 17 files changed, 843 insertions(+), 158 deletions(-) create mode 100644 src/renderer/components/BackButton.tsx create mode 100644 src/renderer/feature/builds/components/BuildCard.tsx create mode 100644 src/renderer/feature/builds/components/BuildProgress.tsx create mode 100644 src/renderer/feature/builds/components/ExportBuildButton.tsx create mode 100644 src/renderer/pages/BuildPage.tsx create mode 100644 src/renderer/pages/BuildsPage.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c61669b..ebc3212 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -13,25 +13,27 @@ import { useCharacterContext } from './contexts/characterContext'; import { ItemsPage } from './pages/ItemsPage'; import { CharacterPage } from './pages/CharacterPage'; import { LoaderPage } from './pages/LoaderPage'; -import { DamagePage } from './feature/damage/DamagePage' +import { DamagePage } from './feature/damage/DamagePage'; import { ItemPage } from './pages/ItemPage'; import { FishingPage } from './pages/FishingPage'; import { LastRunInfoPage } from './pages/LastRunInfoPage'; import { useSettingsContext } from './contexts/settingsContext'; import { iconFromId } from './icons/icons'; +import { BuildsPage } from './pages/BuildsPage'; +import { BuildPage } from './pages/BuildPage'; export default function App() { const { loadClasses } = useCharacterContext(); const { wc3path } = useSettingsContext(); const onRefreshClick = () => { - loadClasses() + loadClasses(); window.electron.ipcRenderer.sendMessage('request_last_run', wc3path); window.electron.ipcRenderer.sendMessage('get_latest_damage_by_type'); - } + }; return ( - + @@ -68,6 +70,9 @@ export default function App() { Items + + Builds + Fishing @@ -97,16 +102,25 @@ export default function App() { }} > - - }/> - }/> - }/> - }/> - }/> - } /> - }/> - } /> - + + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + diff --git a/src/renderer/components/BackButton.tsx b/src/renderer/components/BackButton.tsx new file mode 100644 index 0000000..1bfadb4 --- /dev/null +++ b/src/renderer/components/BackButton.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react'; +import { IconButton, Typography, ButtonProps } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +interface BackButtonProps extends ButtonProps { + onClick?: () => void; +} + +export const BackButton: FC = ({ ...props }) => { + const { onClick } = props; + const navigate = useNavigate(); + return ( + (onClick ? onClick() : navigate(-1))} + > + + + + ); +}; diff --git a/src/renderer/components/CompactItem.tsx b/src/renderer/components/CompactItem.tsx index be49a12..0a8143b 100644 --- a/src/renderer/components/CompactItem.tsx +++ b/src/renderer/components/CompactItem.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useCallback } from 'react'; import Avatar from '@mui/material/Avatar'; import Tooltip from '@mui/material/Tooltip'; import { grey } from '@mui/material/colors'; @@ -8,14 +8,21 @@ import { useNavigate } from 'react-router-dom'; import { useItemContext } from '../contexts/itemsContext'; interface Props { - id?: string, - noTooltip?: boolean, - onClick?: () => void, + id?: string; + noTooltip?: boolean; + onClick?: () => void; } export const CompactItem: FC = ({ id, noTooltip, onClick }) => { const { items } = useItemContext(); const navigate = useNavigate(); + const handleClick = useCallback(() => { + if (onClick) { + onClick(); + } else { + navigate(`/item/${id}`); + } + }, [onClick]); if (!id) { return ( @@ -35,20 +42,16 @@ export const CompactItem: FC = ({ id, noTooltip, onClick }) => { return ( - : null - } + title={!noTooltip ? : null} placement="right-start" > navigate(`/item/${id}`)} + onClick={handleClick} /> ); diff --git a/src/renderer/components/Stash.tsx b/src/renderer/components/Stash.tsx index 5641592..7f3048d 100644 --- a/src/renderer/components/Stash.tsx +++ b/src/renderer/components/Stash.tsx @@ -1,19 +1,52 @@ import Box from '@mui/material/Box'; import { BoxProps } from '@mui/material'; import { CompactItem } from './CompactItem'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; interface EvoStashProps extends BoxProps { itemIds: string[]; + onItemClick?: (index: number) => void; + playerItems?: string[] | null; } export function EvoStash(props: EvoStashProps) { - const { itemIds, ...rest } = props; + const { itemIds, onItemClick, playerItems, ...rest } = props; + const navigate = useNavigate(); + + const handleItemClick = useCallback( + (id: string, index: number) => { + if (playerItems) { + navigate(`/item/${id}`, { + state: { playerItems: [...(playerItems || [])] }, + }); + } else if (onItemClick) { + onItemClick(index); + } + }, + [onItemClick], + ); + + const getOnClickHandler = useCallback( + (id: string, index: number) => { + if (playerItems || onItemClick) { + return () => handleItemClick(id, index); + } + return undefined; + }, + [handleItemClick, playerItems, onItemClick], + ); + return ( {itemIds.map((id, index) => ( // eslint-disable-next-line react/no-array-index-key - + ))} ); -}; +} diff --git a/src/renderer/contexts/buildsContext.tsx b/src/renderer/contexts/buildsContext.tsx index d734b36..9abb4f6 100644 --- a/src/renderer/contexts/buildsContext.tsx +++ b/src/renderer/contexts/buildsContext.tsx @@ -9,7 +9,7 @@ import { } from 'react'; export interface TBuild { - id: number; + id: number | null; name: string; items: string[]; } @@ -57,11 +57,8 @@ export const BuildsProvider: FC = ({ children }) => { }; const saveBuild = (build: TBuild) => { + console.log('saveBuild', build); window.electron.ipcRenderer.sendMessage('save_build', build); - setBuildsState({ - ...builds, - [build.id]: build, - }); }; const saveSelectedBuild = ( @@ -74,13 +71,6 @@ export const BuildsProvider: FC = ({ children }) => { playerClass: hero, buildId, }); - setSelectedBuilds({ - ...selectedBuilds, - [playerName]: { - ...selectedBuilds[playerName], - [hero]: buildId, - }, - }); }; const value = useMemo( diff --git a/src/renderer/feature/builds/components/BuildCard.tsx b/src/renderer/feature/builds/components/BuildCard.tsx new file mode 100644 index 0000000..ee38c28 --- /dev/null +++ b/src/renderer/feature/builds/components/BuildCard.tsx @@ -0,0 +1,65 @@ +import Card from '@mui/material/Card'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import { useNavigate } from 'react-router-dom'; +import { EvoStash } from '../../../components/Stash'; +import StarIcon from '@mui/icons-material/Star'; +import StarBorderIcon from '@mui/icons-material/StarBorder'; +import { useSettingsContext } from '../../../contexts/settingsContext'; +import { TBuild } from '../../../contexts/buildsContext'; +import { ExportBuildButton } from './ExportBuildButton'; + +interface BuildCardProps { + build: TBuild; + playerItems?: string[] | null; +} + +export function BuildCard({ build, playerItems }: BuildCardProps) { + const navigate = useNavigate(); + const { addFavouriteClass, removeFavouriteClass } = useSettingsContext(); + + const handleNavigate = () => { + navigate(`/builds/${build.id}`, { + state: { playerItems: [...(playerItems || [])] }, + }); + }; + + return ( + + + + {build.name} + console.log('favourite')}> + {true ? : } + + + + + + + + + + + ); +} diff --git a/src/renderer/feature/builds/components/BuildProgress.tsx b/src/renderer/feature/builds/components/BuildProgress.tsx new file mode 100644 index 0000000..5cb6170 --- /dev/null +++ b/src/renderer/feature/builds/components/BuildProgress.tsx @@ -0,0 +1,133 @@ +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Collapse from '@mui/material/Collapse'; +import Divider from '@mui/material/Divider'; +import { lightBlue } from '@mui/material/colors'; +import { styled } from '@mui/material/styles'; +import IconButton, { IconButtonProps } from '@mui/material/IconButton'; +import { useMemo, useState } from 'react'; +import { useItemContext } from '../../../contexts/itemsContext'; +import { + DependencyObj, + getItemArrFlatDependenciesObject, +} from '../../../util/crafting'; +import { ItemIconAndTitle } from '../../item/components/ItemIconAndTitle'; + +interface ExpandMoreProps extends IconButtonProps { + expand: boolean; +} + +const ExpandMore = styled((props: ExpandMoreProps) => { + const { expand, ...other } = props; + return ; +})(({ theme, expand }) => ({ + transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), +})); + +export const BuildProgress = ({ + itemIdsList, + buildItems, + defaultExpanded = false, +}: { + itemIdsList: Array; + buildItems: string[]; + defaultExpanded?: boolean; +}) => { + const [expanded, setExpanded] = useState(defaultExpanded); + const { items } = useItemContext(); + + const isBuildEmpty = () => { + return ( + buildItems.length === 0 || + buildItems.filter((item) => item !== '').length === 0 + ); + }; + + const [missingForBuild, _] = useMemo(() => { + if (isBuildEmpty()) { + return [{}, []] as [DependencyObj, string[]]; + } else { + return getItemArrFlatDependenciesObject(buildItems, itemIdsList, items); + } + }, [itemIdsList, buildItems]); + + const handleExpandClick = () => { + setExpanded((prev) => !prev); + }; + + if (missingForBuild && Object.keys(missingForBuild).length === 0) { + return null; + } + + return ( + <> + + Build progress + + + + + + {Object.keys(missingForBuild).map((key) => { + return ( + + ); + })} + + + + + ); +}; + +function MissingItem(props: { id: string; amount: number }) { + const { id, amount } = props; + const { items } = useItemContext(); + const item = items[id]; + + if (!item) { + return ( + + {amount} x {id} + + ); + } + + return ( + + + {amount} + + + {item.sourceShort && ( + + {item.sourceShort} + + )} + + ); +} diff --git a/src/renderer/feature/builds/components/ExportBuildButton.tsx b/src/renderer/feature/builds/components/ExportBuildButton.tsx new file mode 100644 index 0000000..99d4af9 --- /dev/null +++ b/src/renderer/feature/builds/components/ExportBuildButton.tsx @@ -0,0 +1,44 @@ +import { FC, useState } from 'react'; +import { IconButton, ButtonProps, Tooltip } from '@mui/material'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; + +interface ExportBuildButtonProps extends ButtonProps { + tooltipTitle: string; + name: string; + items: string[] | null; +} + +export const ExportBuildButton: FC = ({ ...props }) => { + const { name, items, tooltipTitle } = props; + const [dynamicTooltipTitle, setDynamicTooltipTitle] = useState(); + const [tooltipOpen, setTooltipOpen] = useState(false); + + const handleExport = (e: React.MouseEvent) => { + e.stopPropagation(); + const build = { name, items }; + navigator.clipboard.writeText(JSON.stringify(build)); + setTooltipOpen(false); + setTimeout(() => { + setTooltipOpen(true); + setDynamicTooltipTitle('Copied to clipboard!'); + }, 80); + }; + return ( + { + setDynamicTooltipTitle(tooltipTitle); + setTooltipOpen(true); + }} + onMouseLeave={() => setTooltipOpen(false)} + title={dynamicTooltipTitle} + disableInteractive + placement="top" + arrow + > + + + + + ); +}; diff --git a/src/renderer/feature/item/components/CraftsInto.tsx b/src/renderer/feature/item/components/CraftsInto.tsx index adac7e5..7667858 100644 --- a/src/renderer/feature/item/components/CraftsInto.tsx +++ b/src/renderer/feature/item/components/CraftsInto.tsx @@ -1,3 +1,4 @@ +import { FC, useCallback } from 'react'; import { CompactItem } from '../../../components/CompactItem'; import Box from '@mui/material/Box'; import { useNavigate } from 'react-router-dom'; @@ -6,54 +7,98 @@ import { TItem } from '../../../../types'; import { useItemContext } from '../../../contexts/itemsContext'; import { ItemIconAndTitle } from './ItemIconAndTitle'; -export function CraftsInto(props: { item: TItem }) { - const { item } = props; - const { items } = useItemContext(); - const usedForItemsArr = item.partOf.map(id => items[id]) - - if (usedForItemsArr.length === 0) return null; - return ( - - Used for - { - usedForItemsArr.length > 4 - ? - : - } - - ) +interface CraftsIntoProps { + item: TItem; + onItemSelect?: (id: string) => void; } -function TiledItems({ items }: { items: TItem[] }) { - if (!items || items.length === 0) return null; - return ( - - {items.map((item) => ( - - ))} - - ) +export const CraftsInto: FC = (props) => { + const { item, onItemSelect } = props; + const { items } = useItemContext(); + + const usedForItemsArr = item.partOf.map((id) => items[id]); + + if (usedForItemsArr.length === 0) return null; + + return ( + + Used for + {usedForItemsArr.length > 4 ? ( + + ) : ( + + )} + + ); +}; + +function TiledItems({ + items, + onItemSelect, +}: { + items: TItem[]; + onItemSelect?: (id: string) => void; +}) { + if (!items || items.length === 0) return null; + + const handleClick = useCallback( + (item: TItem) => { + if (onItemSelect) { + onItemSelect(item.id); + } + }, + [onItemSelect], + ); + return ( + + {items.map((item) => ( + handleClick(item) : undefined} + /> + ))} + + ); } -function ListedItems({ items }: { items: TItem[] }) { - if (items.length === 0) return null; - const navigate = useNavigate(); - - return ( - - {items.map((item) => ( - { navigate(`/item/${item.id}`) }} - item={item}/> - ))} - - ) -} \ No newline at end of file +function ListedItems({ + items, + onItemSelect, +}: { + items: TItem[]; + onItemSelect?: (id: string) => void; +}) { + if (items.length === 0) return null; + const navigate = useNavigate(); + + const handleClick = useCallback( + (item: TItem) => { + if (onItemSelect) { + onItemSelect(item.id); + } else { + navigate(`/item/${item.id}`); + } + }, + [onItemSelect], + ); + + return ( + + {items.map((item) => ( + handleClick(item)} + item={item} + /> + ))} + + ); +} diff --git a/src/renderer/feature/item/components/Item.tsx b/src/renderer/feature/item/components/Item.tsx index df84975..c742283 100644 --- a/src/renderer/feature/item/components/Item.tsx +++ b/src/renderer/feature/item/components/Item.tsx @@ -7,19 +7,20 @@ import { CraftsInto } from './CraftsInto'; import { ItemDependenciesTree } from './DependencyTree'; interface EvoItemProps extends BoxProps { - item: TItem + item: TItem; + onItemSelect?: (id: string) => void; } export function EvoItem(props: EvoItemProps) { - const { item, sx,...rest } = props; + const { item, onItemSelect, sx, ...rest } = props; const { color } = item.rarity; return ( - - - + + + {item.id} @@ -37,10 +38,7 @@ export function EvoItem(props: EvoItemProps) { ))} - + - ) + ); } - - - diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 0364d8a..67d01f6 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -7,6 +7,7 @@ import '@fontsource/roboto/700.css'; import { CharacterProvider } from './contexts/characterContext'; import { SettingsProvider } from './contexts/settingsContext'; import { ItemsProvider } from './contexts/itemsContext'; +import { BuildsProvider } from './contexts/buildsContext'; const container = document.getElementById('root') as HTMLElement; const root = createRoot(container); @@ -15,7 +16,9 @@ root.render( - + + + , diff --git a/src/renderer/pages/BuildPage.tsx b/src/renderer/pages/BuildPage.tsx new file mode 100644 index 0000000..1dcc04c --- /dev/null +++ b/src/renderer/pages/BuildPage.tsx @@ -0,0 +1,218 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { EvoStash } from '../components/Stash'; +import { ItemsPage } from './ItemsPage'; +import { useLocation, useParams } from 'react-router-dom'; +import { useBuildsContext } from '../contexts/buildsContext'; +import { + Box, + Button, + Divider, + IconButton, + Modal, + TextField, + Tooltip, +} from '@mui/material'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; +import { ItemPage } from './ItemPage'; +import { BackButton } from '../components/BackButton'; +import { BuildProgress } from '../feature/builds/components/BuildProgress'; + +const EMPTY_ITEM = ''; + +export const BuildPage: FC = () => { + const { id } = useParams<{ id: string }>(); + const { builds, saveBuild } = useBuildsContext(); + const buildId = id ? Number(id) : null; + const existingBuild = builds && buildId ? builds[buildId] : undefined; + const [items, setItemIds] = useState( + existingBuild ? existingBuild.items : Array(6).fill(EMPTY_ITEM), + ); + const nameRef = useRef(null); + const importRef = useRef(null); + const location = useLocation(); + const playerItems = location.state?.playerItems; + const [importModalOpen, setImportModalOpen] = useState(false); + const [itemSelected, setItemSelected] = useState(null); + + useEffect(() => { + if (existingBuild) { + setItemIds(existingBuild.items); + if (nameRef.current) { + nameRef.current.value = existingBuild.name; + } + } + }, []); + + const handleItemAdd = (id: string) => { + const emptyIndex = items.findIndex((itemId) => !itemId); + if (emptyIndex !== -1) { + const newItems = [...items]; + newItems[emptyIndex] = id; + setItemIds(newItems); + } + }; + + const handleItemRemove = (index: number) => { + const newItems = [...items]; + newItems[index] = EMPTY_ITEM; + setItemIds(newItems); + }; + + const getBuild = () => { + const name = nameRef.current ? nameRef.current.value : ''; + return { id: buildId, name: name || `Build ${buildId}`, items }; + }; + + const handleSaveBuild = () => { + const build = getBuild(); + saveBuild(build); + }; + + const handleExport = () => { + const name = nameRef.current ? nameRef.current.value : ''; + const build = { name: name || `Build ${buildId}`, items }; + navigator.clipboard.writeText(JSON.stringify(build)); + }; + + const handleImport = () => { + const text = importRef.current?.value; + if (!text) { + return; + } + + try { + const build = JSON.parse(text); + if (Array.isArray(build.items) && build.items.length === 6) { + setItemIds(build.items); + if (nameRef.current) { + nameRef.current.value = build.name; + } + } + setImportModalOpen(false); + } catch (e) { + console.error(e); + } + }; + + return ( + + + + + + + + + + + setImportModalOpen(true)}> + + + + + + + + + + {itemSelected ? ( + setItemSelected(null)} + onItemSelect={setItemSelected} + onAddToBuild={handleItemAdd} + /> + ) : ( + + )} + + + + + + + setImportModalOpen(false)}> + + + + + + + ); +}; diff --git a/src/renderer/pages/BuildsPage.tsx b/src/renderer/pages/BuildsPage.tsx new file mode 100644 index 0000000..7b4b18a --- /dev/null +++ b/src/renderer/pages/BuildsPage.tsx @@ -0,0 +1,40 @@ +import { FC, useEffect } from 'react'; +import { Box, Button, Typography } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { useBuildsContext } from '../contexts/buildsContext'; +import { BuildCard } from '../feature/builds/components/BuildCard'; + +export const BuildsPage: FC = () => { + const navigate = useNavigate(); + const { builds } = useBuildsContext(); + + const handleCreateNewBuild = () => { + navigate(`/builds/new`); + }; + + const isEmpty = !builds || Object.keys(builds).length === 0; + + return ( + + + + {isEmpty ? ( + + No builds available. Create a new build to get started. + + ) : ( + Object.keys(builds).map((key) => { + const buildId = Number(key); + return ; + }) + )} + + + ); +}; diff --git a/src/renderer/pages/CharacterPage.tsx b/src/renderer/pages/CharacterPage.tsx index ea2b01e..045f982 100644 --- a/src/renderer/pages/CharacterPage.tsx +++ b/src/renderer/pages/CharacterPage.tsx @@ -12,6 +12,7 @@ import IconButton from '@mui/material/IconButton'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import Tooltip from '@mui/material/Tooltip'; import { GodlyProgress } from '../feature/item/components/GodlyProgress'; +import { BackButton } from '../components/BackButton'; export const CharacterPage: FC = () => { const { getCharacter, onLoadClick } = useCharacterContext(); @@ -22,14 +23,13 @@ export const CharacterPage: FC = () => { return null; } - const items = character ? [...character.inventory, ...character.stashes.flat()] : null; + const items = character + ? [...character.inventory, ...character.stashes.flat()] + : null; return (
- - - Go back - + {character.hero} {character.level && ` - ${character.level} level`} @@ -68,7 +68,7 @@ export const CharacterPage: FC = () => { - + {items && } @@ -79,25 +79,25 @@ export const CharacterPage: FC = () => { alignItems: 'center', }} > - - Press Load - it will set hotkey for A button. - Head to wc3 and press A. - Let it do its thing. - - Tip: remember to turn off caps lock and switch to English - - - }> - + + + Press Load - it will set hotkey for A button. + + Head to wc3 and press A. + Let it do its thing. + + Tip: remember to turn off caps lock and switch to English + + + } + > +
diff --git a/src/renderer/pages/ItemPage.tsx b/src/renderer/pages/ItemPage.tsx index 75d547e..70496d0 100644 --- a/src/renderer/pages/ItemPage.tsx +++ b/src/renderer/pages/ItemPage.tsx @@ -1,27 +1,66 @@ -import { Link, useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router-dom'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; +import { useLocation, useParams } from 'react-router-dom'; import { useItemContext } from '../contexts/itemsContext'; +import { BackButton } from '../components/BackButton'; +import { Box, Button, Divider } from '@mui/material'; +import { FC } from 'react'; import { EvoItem } from '../feature/item/components/Item'; +import { BuildProgress } from '../feature/builds/components/BuildProgress'; -export function ItemPage() { - const { id } = useParams(); +interface ItemPageParams { + itemId?: string | null; + onBackClick?: () => void; + onItemSelect?: (id: string) => void; + onAddToBuild?: (id: string) => void; +} + +export const ItemPage: FC = ({ ...props }) => { + const { itemId, onBackClick, onItemSelect, onAddToBuild } = props; + const { id: paramId } = useParams(); const { items } = useItemContext(); + const location = useLocation(); + const playerItems = location.state?.playerItems; + + const id = itemId || paramId; + if (!id || !items.hasOwnProperty(id)) { - return

How did you end up here?

+ return

How did you end up here?

; } - - const navigate = useNavigate(); return (
- navigate(-1)}> - - - - + + + + {onAddToBuild && ( + + )} + + + {playerItems && ( + <> + + + + + + )} +
); -} \ No newline at end of file +}; diff --git a/src/renderer/pages/ItemsPage.tsx b/src/renderer/pages/ItemsPage.tsx index 7b98393..504390f 100644 --- a/src/renderer/pages/ItemsPage.tsx +++ b/src/renderer/pages/ItemsPage.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useMemo, useRef, useState } from 'react'; +import { ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'; import Box from '@mui/material/Box'; import { Input, InputProps } from '@mui/material'; import { CompactItem } from '../components/CompactItem'; @@ -26,7 +26,13 @@ function DebounceInput(props: InputProps & DebounceProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } -export function ItemsPage() { + +type ItemsPageProps = { + onItemSelect?: (id: string) => void; +}; + +export function ItemsPage(props: ItemsPageProps) { + const { onItemSelect } = props; const { items } = useItemContext(); const [filter, setFilter] = useState(''); @@ -36,6 +42,15 @@ export function ItemsPage() { ); }, [filter]); + const handleClick = useCallback( + (id: string) => { + if (onItemSelect) { + onItemSelect(id); + } + }, + [onItemSelect], + ); + return ( {filteredItems.map((id) => ( - + handleClick(id) : undefined} + /> ))} diff --git a/src/renderer/util/crafting.ts b/src/renderer/util/crafting.ts index 72395ab..f692d3f 100644 --- a/src/renderer/util/crafting.ts +++ b/src/renderer/util/crafting.ts @@ -1,33 +1,48 @@ -import { TItem } from "../../types"; +import { TItem } from '../../types'; export type DependencyObj = { - [key: string]: number -} + [key: string]: number; +}; -export const getItemArrFlatDependenciesObject = (items: string[], presentItems: string[] = [], itemsDict: {[key: string]: TItem}): [DependencyObj, string[]] => { +export const getItemArrFlatDependenciesObject = ( + items: string[], + presentItems: string[] = [], + itemsDict: { [key: string]: TItem }, +): [DependencyObj, string[]] => { let res: DependencyObj = {}; - let stash: string[] = presentItems; + let stash: string[] = Array.from(presentItems).filter((item) => item !== ''); - for (const item of items) { - const [tempItems, tempStash] = getItemFlatDependenciesObject(item, stash, itemsDict); + for (const item of items.filter((item) => item !== '')) { + const [tempItems, tempStash] = getItemFlatDependenciesObject( + item, + stash, + itemsDict, + ); res = addDependencies(res, tempItems); stash = tempStash; } return [res, stash]; -} -export const getItemFlatDependenciesObject = (item: string, presentItems: string[] = [], itemsDict: {[key: string]: TItem}): [DependencyObj, string[]] => { - let res: DependencyObj = {}; - let items:Array = presentItems; +}; +export const getItemFlatDependenciesObject = ( + item: string, + presentItems: string[] = [], + itemsDict: { [key: string]: TItem }, +): [DependencyObj, string[]] => { + let res: DependencyObj = {}; + let items: Array = presentItems; const traverse = (curr: string) => { const index = items.indexOf(curr); - if (index !== -1) { + if (index !== -1) { items.splice(index, 1); return; } - if (!itemsDict.hasOwnProperty(curr) || itemsDict[curr].recipe.length === 0) { + if ( + !itemsDict.hasOwnProperty(curr) || + itemsDict[curr].recipe.length === 0 + ) { if (res.hasOwnProperty(curr)) { res[curr] += 1; } else { @@ -39,15 +54,18 @@ export const getItemFlatDependenciesObject = (item: string, presentItems: string for (const dep of itemsDict[curr].recipe) { traverse(dep); } - } + }; traverse(item); return [res, items]; -} +}; -export const addDependencies = (a:DependencyObj, b: DependencyObj): DependencyObj => { - const res: DependencyObj = {...a}; +export const addDependencies = ( + a: DependencyObj, + b: DependencyObj, +): DependencyObj => { + const res: DependencyObj = { ...a }; for (const key in b) { if (res.hasOwnProperty(key)) { @@ -58,4 +76,4 @@ export const addDependencies = (a:DependencyObj, b: DependencyObj): DependencyOb } return res; -} \ No newline at end of file +};