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,