Skip to content

Commit d668649

Browse files
committed
add Avatar component
1 parent 04e8cf2 commit d668649

File tree

4 files changed

+115
-34
lines changed

4 files changed

+115
-34
lines changed

packages/keychain/src/components/Menu/index.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export function Menu({ onLogout }: { onLogout: () => void }) {
1212
title={controller.username}
1313
description={<CopyAddress address={controller.address} />}
1414
>
15-
<Content h="350px"></Content>
15+
<Content h="350px">
16+
17+
</Content>
1618
<Footer>
1719
<Button w="full" onClick={onLogout}>
1820
Log out
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Box, Image, Spinner } from "@chakra-ui/react";
2+
3+
import { useConnection } from "hooks/connection";
4+
import { PropsWithChildren, useEffect, useState } from "react";
5+
import { byteArray, cairo, shortString } from "starknet";
6+
import { selectors, VERSION } from "utils/selectors";
7+
import Storage from "utils/storage";
8+
9+
const avatarAddress =
10+
"0x56be7d500bd759ac4f96f786f15e9e4702c1ae0582091b20c90546e44ba42fc";
11+
12+
export const stringFromByteArray = (arr: string[]) => {
13+
arr = arr.slice(1, -1);
14+
while (arr.length > 0 && arr[arr.length - 1] === "0x0") {
15+
arr = arr.slice(0, -1);
16+
}
17+
18+
return arr.map((i) => shortString.decodeShortString(i)).join("");
19+
};
20+
21+
export function Avatar({ children }: PropsWithChildren) {
22+
const { controller } = useConnection();
23+
24+
const [image, setImage] = useState("");
25+
const [isLoading, setIsLoading] = useState(false);
26+
27+
useEffect(() => {
28+
const avatarSelector = selectors[VERSION].avatar(controller.address);
29+
30+
const init = async () => {
31+
const fromStorage = Storage.get(avatarSelector);
32+
if (fromStorage && fromStorage !== "") {
33+
setImage(fromStorage.image);
34+
return;
35+
}
36+
37+
setIsLoading(true);
38+
try {
39+
const tokenId = cairo.uint256(controller.address);
40+
let metadataRaw = await controller.account.callContract({
41+
contractAddress: avatarAddress,
42+
entrypoint: "token_uri",
43+
calldata: [tokenId.low, tokenId.high],
44+
});
45+
46+
const metadataString = stringFromByteArray(metadataRaw);
47+
const metadataJson = JSON.parse(metadataString);
48+
49+
Storage.set(avatarSelector, metadataJson);
50+
setImage(metadataJson.image);
51+
} catch (e) {
52+
// console.log(e);
53+
setImage("");
54+
}
55+
setIsLoading(false);
56+
};
57+
58+
init();
59+
}, [controller.address]);
60+
61+
return (
62+
<Box width={"48px"} height={"48px"} display={"flex"} flexShrink={0} position={"relative"}>
63+
{!isLoading && image !== "" ? (
64+
<Image src={image} />
65+
) : (
66+
<>{children}</>
67+
)}
68+
{isLoading && (
69+
<Spinner position={"absolute"} size="sm" top={"16px"} left={"16px"} />
70+
)}
71+
</Box>
72+
);
73+
}

packages/keychain/src/components/layout/Container/Header/Banner.tsx

+38-33
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import {
99
useColorMode,
1010
Square,
1111
HStack,
12+
1213
} from "@chakra-ui/react";
1314
import { useControllerTheme } from "hooks/theme";
1415
import { useMemo } from "react";
1516
import { useLayoutVariant } from "../";
1617
import { TOP_BAR_HEIGHT } from "./TopBar";
18+
import { Avatar } from "components/avatar";
1719

1820
export type BannerProps = {
1921
Icon?: React.ComponentType<IconProps>;
@@ -106,23 +108,24 @@ export function Banner({ Icon, icon, title, description }: BannerProps) {
106108
/>
107109

108110
<HStack w="full" p={4} gap={4} minW={0}>
109-
{!!Icon ? (
110-
<Square size="44px" bg="solid.primary" borderRadius="md">
111-
<Icon boxSize={8} />
112-
</Square>
113-
) : !!icon ? (
114-
<Square size="44px" bg="solid.primary" borderRadius="md">
115-
{icon}
116-
</Square>
117-
) : (
118-
<Image
119-
src={theme.icon}
120-
boxSize="44px"
121-
alt="Controller Icon"
122-
borderRadius="md"
123-
/>
124-
)}
125-
111+
<Avatar>
112+
{!!Icon ? (
113+
<Square size="48px" bg="solid.primary" borderRadius="md">
114+
<Icon boxSize={8} />
115+
</Square>
116+
) : !!icon ? (
117+
<Square size="48px" bg="solid.primary" borderRadius="md">
118+
{icon}
119+
</Square>
120+
) : (
121+
<Image
122+
src={theme.icon}
123+
boxSize="48px"
124+
alt="Controller Icon"
125+
borderRadius="md"
126+
/>
127+
)}
128+
</Avatar>
126129
<VStack w="full" align="stretch" gap={1} minW={0}>
127130
<Text
128131
w="full"
@@ -160,22 +163,24 @@ export function Banner({ Icon, icon, title, description }: BannerProps) {
160163
/>
161164

162165
<HStack w="full" p={4} gap={4} minW={0}>
163-
{!!Icon ? (
164-
<Square size="44px" bg="solid.primary" borderRadius="md">
165-
<Icon boxSize={8} />
166-
</Square>
167-
) : !!icon ? (
168-
<Square size="44px" bg="solid.primary" borderRadius="md">
169-
{icon}
170-
</Square>
171-
) : (
172-
<Image
173-
src={theme.icon}
174-
boxSize="44px"
175-
alt="Controller Icon"
176-
borderRadius="md"
177-
/>
178-
)}
166+
<Avatar>
167+
{!!Icon ? (
168+
<Square size="48px" bg="solid.primary" borderRadius="md">
169+
<Icon boxSize={8} />
170+
</Square>
171+
) : !!icon ? (
172+
<Square size="48px" bg="solid.primary" borderRadius="md">
173+
{icon}
174+
</Square>
175+
) : (
176+
<Image
177+
src={theme.icon}
178+
boxSize="48px"
179+
alt="Controller Icon"
180+
borderRadius="md"
181+
/>
182+
)}
183+
</Avatar>
179184

180185
<VStack w="full" align="stretch" gap={1} minW={0}>
181186
<Text

packages/keychain/src/utils/selectors.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const selectors = {
44
[VERSION]: {
55
active: () => `@cartridge/active`,
66
account: (address: string) => `@cartridge/account/${address}`,
7+
avatar: (address: string) => `@cartridge/avatar/${address}`,
78
deployment: (address: string, chainId: string) =>
89
`@cartridge/deployment/${address}/${chainId}`,
910
admin: (address: string, origin: string) =>

0 commit comments

Comments
 (0)