From a9f202ec0c51e578b0b6166756a96250d47f9f82 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 9 Jul 2024 07:29:49 -0600 Subject: [PATCH 1/3] Remove TileDetailsPanel data --- .../index.tsx => TileDetailsPanel.tsx} | 8 ++-- .../src/components/TileDetailsPanel/data.ts | 40 ------------------- 2 files changed, 4 insertions(+), 44 deletions(-) rename packages/client/src/components/{TileDetailsPanel/index.tsx => TileDetailsPanel.tsx} (97%) delete mode 100644 packages/client/src/components/TileDetailsPanel/data.ts diff --git a/packages/client/src/components/TileDetailsPanel/index.tsx b/packages/client/src/components/TileDetailsPanel.tsx similarity index 97% rename from packages/client/src/components/TileDetailsPanel/index.tsx rename to packages/client/src/components/TileDetailsPanel.tsx index 7eab4b5d4..b3696be06 100644 --- a/packages/client/src/components/TileDetailsPanel/index.tsx +++ b/packages/client/src/components/TileDetailsPanel.tsx @@ -27,10 +27,10 @@ import { hexToString, } from 'viem'; -import { useCharacter } from '../../contexts/CharacterContext'; -import { useMUD } from '../../contexts/MUDContext'; -import { fetchMetadataFromUri, uriToHttp } from '../../utils/helpers'; -import { type Character, type Monster } from '../../utils/types'; +import { useCharacter } from '../contexts/CharacterContext'; +import { useMUD } from '../contexts/MUDContext'; +import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; +import { type Character, type Monster } from '../utils/types'; const ROW_HEIGHT = { base: 5, md: 8, lg: 10 }; diff --git a/packages/client/src/components/TileDetailsPanel/data.ts b/packages/client/src/components/TileDetailsPanel/data.ts deleted file mode 100644 index ba8c4a644..000000000 --- a/packages/client/src/components/TileDetailsPanel/data.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const MONSTERS = [ - { - name: 'Kobold', - color: 'yellow', - level: 2, - }, - { - name: 'Green Slime', - color: 'green', - level: 2, - }, - { - name: 'Cave Bandit', - color: 'red', - level: 2, - }, -]; - -export const PLAYERS = [ - { - name: 'Mon-o 🧙‍♂️', - level: 1, - }, - { - name: 'GUATY 🎭', - level: 2, - }, - { - name: 'Wolf R ※', - level: 1, - }, - { - name: 'GUATY 🎭', - level: 1, - }, - { - name: 'Wolf R ※', - level: 2, - }, -]; From 18033dd4646ebcb0bee8c8aff594c91d23d708cb Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 9 Jul 2024 07:36:11 -0600 Subject: [PATCH 2/3] Fix Character page issues --- .../client/src/components/Character/Stats.tsx | 6 +- packages/client/src/components/StatsPanel.tsx | 4 + .../client/src/contexts/CharacterContext.tsx | 24 ++--- packages/client/src/hooks/useToast.ts | 90 ++++++++++--------- packages/client/src/pages/Character.tsx | 70 +++++++++++---- packages/client/src/utils/helpers.ts | 38 +++++--- 6 files changed, 148 insertions(+), 84 deletions(-) diff --git a/packages/client/src/components/Character/Stats.tsx b/packages/client/src/components/Character/Stats.tsx index b17dc0f45..327f75ebd 100644 --- a/packages/client/src/components/Character/Stats.tsx +++ b/packages/client/src/components/Character/Stats.tsx @@ -2,13 +2,13 @@ import { HStack, Text, VStack } from '@chakra-ui/react'; export const Stats = ({ agility, - hitPoints, intelligence, + maxHitPoints, strength, }: { agility: string; - hitPoints: string; intelligence: string; + maxHitPoints: string; strength: string; }): JSX.Element => { return ( @@ -27,7 +27,7 @@ export const Stats = ({ HP - Hit - {hitPoints} + {maxHitPoints} diff --git a/packages/client/src/components/StatsPanel.tsx b/packages/client/src/components/StatsPanel.tsx index 43fea2e2d..c4d80c001 100644 --- a/packages/client/src/components/StatsPanel.tsx +++ b/packages/client/src/components/StatsPanel.tsx @@ -15,6 +15,7 @@ import { useComponentValue } from '@latticexyz/react'; import { encodeEntity } from '@latticexyz/store-sync/recs'; import { useMemo } from 'react'; import { IoIosArrowForward } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; import { useCharacter } from '../contexts/CharacterContext'; import { useMUD } from '../contexts/MUDContext'; @@ -23,6 +24,7 @@ import { Level } from './Level'; const CURRENT_LEVEL = 1; export const StatsPanel = (): JSX.Element => { + const navigate = useNavigate(); const isDesktop = useBreakpointValue({ base: false, lg: true }); const { components: { Levels }, @@ -56,6 +58,8 @@ export const StatsPanel = (): JSX.Element => { return ( navigate(`/characters/${character.characterId}`)} spacing={4} _hover={{ cursor: 'pointer', textDecoration: 'underline' }} > diff --git a/packages/client/src/contexts/CharacterContext.tsx b/packages/client/src/contexts/CharacterContext.tsx index 3ca992326..5e171f415 100644 --- a/packages/client/src/contexts/CharacterContext.tsx +++ b/packages/client/src/contexts/CharacterContext.tsx @@ -9,16 +9,14 @@ import { useEffect, useState, } from 'react'; -import { - bytesToHex, - formatEther, - getContract, - hexToBytes, - hexToString, -} from 'viem'; +import { formatEther, getContract, hexToString } from 'viem'; import { useToast } from '../hooks/useToast'; -import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; +import { + decodeCharacterId, + fetchMetadataFromUri, + uriToHttp, +} from '../utils/helpers'; import type { CharacterData, CharacterStats } from '../utils/types'; import { useMUD } from './MUDContext'; @@ -35,6 +33,7 @@ const CharacterContext = createContext({ agility: '0', experience: '0', intelligence: '0', + level: '0', maxHitPoints: '0', strength: '0', }, @@ -82,9 +81,9 @@ export const CharacterProvider = ({ ).map(entity => { const characterData = getComponentValueStrict(Characters, entity); - const entityBytes = hexToBytes(entity.toString() as `0x${string}`); - const tokenBytes = entityBytes.slice(20); - const tokenId = BigInt(bytesToHex(tokenBytes)).toString(); + const { characterTokenId } = decodeCharacterId( + entity.toString() as `0x${string}`, + ); return { characterClass: characterData.class, @@ -92,7 +91,7 @@ export const CharacterProvider = ({ locked: characterData.locked, name: hexToString(characterData.name as `0x${string}`, { size: 32 }), owner: characterData.owner, - tokenId, + tokenId: characterTokenId, }; })[0]; @@ -196,6 +195,7 @@ export const CharacterProvider = ({ agility: characterStats?.agility.toString() ?? '0', experience: characterStats?.experience.toString() ?? '0', intelligence: characterStats?.intelligence.toString() ?? '0', + level: characterStats?.level.toString() ?? '0', maxHitPoints: characterStats?.maxHitPoints.toString() ?? '0', strength: characterStats?.strength.toString() ?? '0', }, diff --git a/packages/client/src/hooks/useToast.ts b/packages/client/src/hooks/useToast.ts index 322d6d5fa..139ca66be 100644 --- a/packages/client/src/hooks/useToast.ts +++ b/packages/client/src/hooks/useToast.ts @@ -1,4 +1,5 @@ import { useToast as useChakraToast } from '@chakra-ui/react'; +import { useCallback } from 'react'; import { getErrorMessage, USER_ERRORS } from '../utils/errors'; @@ -9,46 +10,55 @@ export const useToast = (): { } => { const toast = useChakraToast(); - const renderError = (error: unknown, defaultError?: string) => { - const errorMsg = getErrorMessage(error); - // eslint-disable-next-line no-console - console.error(error); - - if (USER_ERRORS.includes(errorMsg)) { - return; - } - - toast({ - description: getErrorMessage(error, defaultError), - position: 'top', - status: 'error', - containerStyle: { - bg: 'red', - }, - }); - }; - - const renderWarning = (msg: string) => { - toast({ - description: msg, - position: 'top', - status: 'warning', - containerStyle: { - bg: 'yellow', - }, - }); - }; - - const renderSuccess = (msg: string) => { - toast({ - description: msg, - position: 'top', - status: 'success', - containerStyle: { - bg: 'green', - }, - }); - }; + const renderError = useCallback( + (error: unknown, defaultError?: string) => { + const errorMsg = getErrorMessage(error); + // eslint-disable-next-line no-console + console.error(error); + + if (USER_ERRORS.includes(errorMsg)) { + return; + } + + toast({ + description: getErrorMessage(error, defaultError), + position: 'top', + status: 'error', + containerStyle: { + bg: 'red', + }, + }); + }, + [toast], + ); + + const renderWarning = useCallback( + (msg: string) => { + toast({ + description: msg, + position: 'top', + status: 'warning', + containerStyle: { + bg: 'yellow', + }, + }); + }, + [toast], + ); + + const renderSuccess = useCallback( + (msg: string) => { + toast({ + description: msg, + position: 'top', + status: 'success', + containerStyle: { + bg: 'green', + }, + }); + }, + [toast], + ); return { renderError, renderWarning, renderSuccess }; }; diff --git a/packages/client/src/pages/Character.tsx b/packages/client/src/pages/Character.tsx index 1fe574f61..005dc39a7 100644 --- a/packages/client/src/pages/Character.tsx +++ b/packages/client/src/pages/Character.tsx @@ -5,27 +5,29 @@ import { Center, Grid, GridItem, + Spinner, Text, } from '@chakra-ui/react'; -import { getComponentValueStrict } from '@latticexyz/recs'; -import { encodeEntity } from '@latticexyz/store-sync/recs'; -import { useEffect, useMemo, useState } from 'react'; +import { Entity, getComponentValueStrict } from '@latticexyz/recs'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { formatEther, getContract, hexToString } from 'viem'; import { ItemCard } from '../components/Character/Card/ItemCard'; import { Misc } from '../components/Character/Misc'; import { Profile } from '../components/Character/Profile'; -import { Stats } from '../components/Character/Stats'; +import { Stats as StatsPanel } from '../components/Character/Stats'; import { useCharacter } from '../contexts/CharacterContext'; import { useMUD } from '../contexts/MUDContext'; +import { useToast } from '../hooks/useToast'; import { fetchMetadataFromUri, uriToHttp } from '../utils/helpers'; import type { Character, CharacterStats } from '../utils/types'; export const CharacterPage = (): JSX.Element => { const { characterId } = useParams(); + const { renderError } = useToast(); const { - components: { Characters, CharacterStats }, + components: { Characters, Stats }, network: { publicClient, worldContract }, } = useMUD(); const { character: userCharacter } = useCharacter(); @@ -33,18 +35,21 @@ export const CharacterPage = (): JSX.Element => { const [character, setCharacter] = useState< (Character & CharacterStats) | null >(null); + const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - (async (): Promise => { + const fetchCharacter = useCallback(async () => { + try { if (!(characterId && publicClient && worldContract)) return; + setIsLoading(true); - const entity = encodeEntity( - { characterId: 'uint256' }, - { characterId: BigInt(characterId) }, + const characterData = getComponentValueStrict( + Characters, + characterId as Entity, + ); + const characterStats = getComponentValueStrict( + Stats, + characterId as Entity, ); - - const characterData = getComponentValueStrict(Characters, entity); - const characterStats = getComponentValueStrict(CharacterStats, entity); const characterTokenAddress = await worldContract.read.UD__getCharacterToken(); @@ -76,7 +81,7 @@ export const CharacterPage = (): JSX.Element => { }); const metadataURI = await characterToken.read.tokenURI([ - BigInt(characterId), + BigInt(characterData.tokenId), ]); const fetachedMetadata = await fetchMetadataFromUri( @@ -121,24 +126,51 @@ export const CharacterPage = (): JSX.Element => { agility: characterStats?.agility.toString() ?? '0', experience: characterStats?.experience.toString() ?? '0', characterClass: characterData.class, - characterId, - hitPoints: characterStats?.hitPoints.toString() ?? '0', + characterId: characterId as Entity, intelligence: characterStats?.intelligence.toString() ?? '0', + level: characterStats?.level.toString() ?? '0', locked: characterData.locked, + maxHitPoints: characterStats?.maxHitPoints.toString() ?? '0', name: hexToString(characterData.name as `0x${string}`, { size: 32, }), owner: characterData.owner, strength: characterStats?.strength.toString() ?? '0', + tokenId: characterData.tokenId.toString(), }); + } catch (error) { + renderError(error, 'Failed to fetch character data'); + } finally { + setIsLoading(false); + } + }, [ + characterId, + Characters, + Stats, + publicClient, + renderError, + worldContract, + ]); + + useEffect(() => { + (async (): Promise => { + await fetchCharacter(); })(); - }, [characterId, Characters, CharacterStats, publicClient, worldContract]); + }, [fetchCharacter]); const isOwner = useMemo( () => character?.owner === userCharacter?.owner, [character, userCharacter], ); + if (isLoading) { + return ( +
+ +
+ ); + } + return ( {character ? ( @@ -188,10 +220,10 @@ export const CharacterPage = (): JSX.Element => { px={6} rowStart={{ base: 2, sm: 2, md: 2, lg: 1, xl: 1 }} > - diff --git a/packages/client/src/utils/helpers.ts b/packages/client/src/utils/helpers.ts index 03d2df142..a06b5b45e 100644 --- a/packages/client/src/utils/helpers.ts +++ b/packages/client/src/utils/helpers.ts @@ -1,5 +1,33 @@ +import { hexToBigInt } from 'viem'; + import type { Metadata } from '../utils/types'; +export const decodeCharacterId = ( + characterId: `0x${string}`, +): { + ownerAddress: string; + characterTokenId: string; +} => { + const bigIntValue = hexToBigInt(characterId); + + const characterTokenId = bigIntValue & ((1n << 96n) - 1n); + + const ownerAddressBigInt = bigIntValue >> 96n; + const ownerAddress = `0x${ownerAddressBigInt.toString(16).padStart(40, '0')}`; + + return { ownerAddress, characterTokenId: characterTokenId.toString() }; +}; + +export const fetchMetadataFromUri = async (uri: string): Promise => { + const res = await fetch(uri); + if (!res.ok) throw new Error('Failed to fetch'); + const metadata = await res.json(); + metadata.name = metadata.name || ''; + metadata.description = metadata.description || ''; + metadata.image = uriToHttp(metadata.image)[0] || ''; + return metadata; +}; + const IPFS_GATEWAYS = [ 'https://black-bright-cuckoo-327.mypinata.cloud', 'https://cloudflare-ipfs.com', @@ -42,15 +70,5 @@ export const uriToHttp = (uri: string): string[] => { } }; -export const fetchMetadataFromUri = async (uri: string): Promise => { - const res = await fetch(uri); - if (!res.ok) throw new Error('Failed to fetch'); - const metadata = await res.json(); - metadata.name = metadata.name || ''; - metadata.description = metadata.description || ''; - metadata.image = uriToHttp(metadata.image)[0] || ''; - return metadata; -}; - export const shortenAddress = (address: string, length = 4): string => `${address.slice(0, length + 2)}...${address.slice(-length)}`; From b9b8def79392447d4589646bbfc3f40038cd0a52 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 9 Jul 2024 08:08:21 -0600 Subject: [PATCH 3/3] Make sure react version gets detected in linter --- packages/client/.eslintrc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/client/.eslintrc b/packages/client/.eslintrc index 83043581b..d8eb52799 100644 --- a/packages/client/.eslintrc +++ b/packages/client/.eslintrc @@ -28,6 +28,9 @@ "no-console": "error", }, "settings": { + "react": { + "version": "detect", + }, "import/resolver": { "typescript": { "alwaysTryTypes": true,