Skip to content

Commit 82ee50b

Browse files
committed
feat: profile page lists NFTs by wallet
1 parent 7a97f8d commit 82ee50b

30 files changed

+1082
-446
lines changed

app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"lodash": "^4.17.21",
3939
"materialize-css": "^1.0.0-rc.2",
4040
"moment": "^2.29.1",
41+
"moralis": "^2.26.1",
4142
"next": "^14.2.1",
4243
"next-i18next": "^8.2.0",
4344
"react": "^18.2.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createContext } from "react";
2+
3+
import { Erc721ContextType } from "./Erc721Context.types";
4+
5+
export const Erc721Context = createContext<Erc721ContextType | undefined>(undefined);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { GetNFTsByWalletResult } from "api/evm/ERC721/types";
2+
import { ReactNode } from "react";
3+
4+
export type Erc721ContextControllerProps = {
5+
children: ReactNode;
6+
};
7+
8+
export type Erc721ContextType = {
9+
nftsByWallet: GetNFTsByWalletResult[][];
10+
getNFTsByWallet: () => Promise<void>;
11+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
import { useAccount } from "wagmi";
3+
import axios from "axios";
4+
import { GetNFTsByWalletRequest, GetNFTsByWalletResult } from "api/evm/ERC721/types";
5+
import _ from "lodash";
6+
7+
import { useRoutes } from "hooks/useRoutes/useRoutes";
8+
9+
import { Erc721ContextControllerProps, Erc721ContextType } from "./Erc721Context.types";
10+
import { Erc721Context } from "./Erc721Context";
11+
12+
const groupNFTsByContract = (nfts: GetNFTsByWalletResult[]) => {
13+
const group = _.groupBy(nfts, "tokenAddress");
14+
15+
console.log(_.map(group));
16+
17+
return _.map(group);
18+
};
19+
20+
export const Erc721ContextController = ({ children }: Erc721ContextControllerProps) => {
21+
const [nftsByWallet, setNftsByWallet] = React.useState<Erc721ContextType["nftsByWallet"]>([]);
22+
23+
const { address, chainId } = useAccount();
24+
const routes = useRoutes();
25+
26+
const getNFTsByWallet = async () => {
27+
try {
28+
const data: GetNFTsByWalletRequest = {
29+
chainId: chainId!,
30+
walletAddress: address!,
31+
};
32+
33+
const result = await axios.post<GetNFTsByWalletResult[]>(routes.api.evm.ERC721.getNFTsByWallet(), {
34+
data,
35+
});
36+
37+
console.log(result.data);
38+
39+
setNftsByWallet(groupNFTsByContract(result.data));
40+
} catch (error) {
41+
console.error(error);
42+
}
43+
};
44+
45+
const props: Erc721ContextType = {
46+
getNFTsByWallet,
47+
nftsByWallet,
48+
};
49+
50+
return <Erc721Context.Provider value={props}>{children}</Erc721Context.Provider>;
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useContext } from "react";
2+
3+
import { Erc721Context } from "./Erc721Context";
4+
5+
export const useErc721Context = () => {
6+
const context = useContext(Erc721Context);
7+
8+
if (context === undefined) {
9+
throw new Error("useErc721Context must be used within a Erc721Context");
10+
}
11+
12+
return context;
13+
};

app/src/hooks/useRoutes/useRoutes.tsx

+9-47
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,14 @@
1-
type RouteMap = {
2-
home: () => string;
3-
market: {
4-
price: (args: { marketId: string }) => string;
5-
};
6-
api: {
7-
promptWars: {
8-
createGuestAccount: () => string;
9-
create: () => string;
10-
reveal: () => string;
11-
resolve: () => string;
12-
};
13-
chat: {
14-
dropboxESign: () => string;
15-
openai: {
16-
completionsAPI: () => string;
17-
assistantsAPI: () => string;
18-
};
19-
googleai: {
20-
completionsAPI: () => string;
21-
};
22-
};
23-
};
24-
dashboard: {
25-
latestTrends: () => string;
26-
promptWars: {
27-
home: () => string;
28-
previousMarkets: () => string;
29-
market: (args: { marketId: string }) => string;
30-
};
31-
market: (args: { marketId: string }) => string;
32-
};
33-
};
34-
35-
export const routes: RouteMap = {
1+
export const routes = {
362
home: () => `/`,
37-
market: {
38-
price: ({ marketId }) => `/market/price/${marketId}`,
3+
profile: {
4+
index: () => `/profile`,
395
},
406
api: {
7+
evm: {
8+
ERC721: {
9+
getNFTsByWallet: () => `/api/evm/ERC721/get-nfts-by-wallet`,
10+
},
11+
},
4112
promptWars: {
4213
createGuestAccount: () => `/api/prompt-wars/create-guest-account`,
4314
create: () => `/api/prompt-wars/create`,
@@ -55,15 +26,6 @@ export const routes: RouteMap = {
5526
},
5627
},
5728
},
58-
dashboard: {
59-
latestTrends: () => `/`,
60-
promptWars: {
61-
home: () => `/`,
62-
previousMarkets: () => `/previous-rounds`,
63-
market: ({ marketId }) => `/${marketId}`,
64-
},
65-
market: ({ marketId }) => `/market/${marketId}`,
66-
},
6729
};
6830

69-
export const useRoutes: () => RouteMap = () => routes;
31+
export const useRoutes = () => routes;

app/src/layouts/home-layout/HomeLayout.tsx

+12-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ToastContextController } from "context/toast/ToastContextController";
88
import { ThemeContextController } from "context/theme/ThemeContextController";
99
import { EvmWalletSelectorContextController } from "context/evm/wallet-selector/EvmWalletSelectorContextController";
1010
import { Footer } from "ui/footer/Footer";
11+
import { Erc721ContextController } from "context/evm/ERC721/Erc721ContextController";
1112

1213
import { ChatLayoutProps } from "./HomeLayout.types";
1314
import styles from "./HomeLayout.module.scss";
@@ -27,17 +28,19 @@ export const HomeLayout: React.FC<ChatLayoutProps> = ({ children }) => {
2728
</Head>
2829
<ThemeContextController>
2930
<EvmWalletSelectorContextController>
30-
<ToastContextController>
31-
<div id="modal-root" />
32-
<div id="dropdown-portal" />
33-
<div className={clsx(styles["home-layout"])}>
34-
<Navbar />
31+
<Erc721ContextController>
32+
<ToastContextController>
33+
<div id="modal-root" />
34+
<div id="dropdown-portal" />
35+
<div className={clsx(styles["home-layout"])}>
36+
<Navbar />
3537

36-
<MainPanel>{children}</MainPanel>
38+
<MainPanel>{children}</MainPanel>
3739

38-
<Footer />
39-
</div>
40-
</ToastContextController>
40+
<Footer />
41+
</div>
42+
</ToastContextController>
43+
</Erc721ContextController>
4144
</EvmWalletSelectorContextController>
4245
</ThemeContextController>
4346
</>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextApiRequest, NextApiResponse } from "next";
2+
3+
import logger from "providers/logger";
4+
import moralis from "providers/moralis";
5+
6+
export default async function Fn(request: NextApiRequest, response: NextApiResponse) {
7+
try {
8+
await moralis.loadClient();
9+
} catch (error) {
10+
logger.error(error);
11+
}
12+
13+
try {
14+
const { data } = request.body;
15+
logger.info(`api/evm/ERC721/get-nfts-by-wallet: ${JSON.stringify(data)}`);
16+
17+
const result = await moralis.client.EvmApi.nft.getWalletNFTs({
18+
chain: data.chainId,
19+
format: "decimal",
20+
mediaItems: false,
21+
address: data.walletAddress,
22+
});
23+
24+
response.status(200).json(result.result);
25+
} catch (error) {
26+
logger.error(error);
27+
28+
response.status(500).json({ error: (error as Error).message });
29+
}
30+
}

app/src/pages/api/evm/ERC721/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { EvmNft } from "@moralisweb3/common-evm-utils/lib";
2+
import { MoralisDataObjectValue } from "@moralisweb3/common-core/lib";
3+
4+
export type GetNFTsByWalletResult = EvmNft & {
5+
metadata: MoralisDataObjectValue & Record<string, any>;
6+
};
7+
8+
export type GetNFTsByWalletRequest = {
9+
walletAddress: string;
10+
chainId: number;
11+
};

app/src/pages/profile/index.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { GetServerSidePropsContext, NextPage } from "next";
2+
import { i18n, useTranslation } from "next-i18next";
3+
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
4+
import Head from "next/head";
5+
6+
import { HomeLayout } from "layouts/home-layout/HomeLayout";
7+
import { Collections } from "ui/lease721/profile/collections/Collections";
8+
9+
const Index: NextPage = () => {
10+
const { t } = useTranslation("head");
11+
12+
return (
13+
<HomeLayout>
14+
<Head>
15+
<title>{t("head.og.title")}</title>
16+
<meta name="description" content={t("head.og.description")} />
17+
<meta property="og:title" content={t("head.og.title")} />
18+
<meta property="og:description" content={t("head.og.description")} />
19+
<meta property="og:url" content="https://lease721.com/" />
20+
</Head>
21+
22+
<Collections />
23+
</HomeLayout>
24+
);
25+
};
26+
27+
export const getServerSideProps = async ({ locale }: GetServerSidePropsContext) => {
28+
await i18n?.reloadResources();
29+
30+
return {
31+
props: {
32+
...(await serverSideTranslations(locale!, ["common", "head", "chat", "prompt-wars"])),
33+
},
34+
};
35+
};
36+
37+
export default Index;

app/src/providers/moralis/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Moralis from "moralis";
2+
3+
const loadClient = async () =>
4+
await Moralis.start({
5+
apiKey: process.env.MORALIS_API_KEY,
6+
});
7+
8+
export default {
9+
loadClient,
10+
client: Moralis,
11+
};

app/src/theme/_mixins.scss

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@mixin hero {
2+
&__hero {
3+
@include atLargeTablet {
4+
height: 50vh;
5+
}
6+
display: flex;
7+
flex-direction: column;
8+
justify-content: center;
9+
height: 100vh;
10+
text-align: left;
11+
background-color: var(--color-background);
12+
}
13+
}

app/src/ui/fileagent/navbar/Navbar.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Typography } from "ui/typography/Typography";
44
import { Grid } from "ui/grid/Grid";
55
import { WalletSelector } from "ui/wallet-selector/WalletSelector";
66
import { Lease721Logo } from "ui/icons/Lease721Logo";
7+
import { ProfileDropdown } from "ui/lease721/navbar/profile-dropdown/ProfileDropdown";
78

89
import { NavbarProps } from "./Navbar.types";
910
import styles from "./Navbar.module.scss";
@@ -29,6 +30,9 @@ export const Navbar: React.FC<NavbarProps> = ({ className }) => (
2930
<div className={styles["navbar__right--item"]}>
3031
<WalletSelector />
3132
</div>
33+
<div className={styles["navbar__right--item"]}>
34+
<ProfileDropdown />
35+
</div>
3236
</div>
3337
</Grid.Col>
3438
</Grid.Row>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@import "src/theme/base";
2+
3+
.profile-dropdown {
4+
display: block;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type Styles = {
2+
"profile-dropdown": string;
3+
"z-depth-0": string;
4+
"z-depth-1": string;
5+
"z-depth-1-half": string;
6+
"z-depth-2": string;
7+
"z-depth-3": string;
8+
"z-depth-4": string;
9+
"z-depth-5": string;
10+
};
11+
12+
export type ClassNames = keyof Styles;
13+
14+
declare const styles: Styles;
15+
16+
export default styles;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { screen, render } from "tests";
2+
3+
import { ProfileDropdown } from "./ProfileDropdown";
4+
5+
describe("ProfileDropdown", () => {
6+
it("renders children correctly", () => {
7+
render(<ProfileDropdown>ProfileDropdown</ProfileDropdown>);
8+
9+
const element = screen.getByText("ProfileDropdown");
10+
11+
expect(element).toBeInTheDocument();
12+
});
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import clsx from "clsx";
2+
3+
import { Icon } from "ui/icon/Icon";
4+
import { Typography } from "ui/typography/Typography";
5+
import { useRoutes } from "hooks/useRoutes/useRoutes";
6+
7+
import { ProfileDropdownProps } from "./ProfileDropdown.types";
8+
import styles from "./ProfileDropdown.module.scss";
9+
10+
export const ProfileDropdown: React.FC<ProfileDropdownProps> = ({ className }) => {
11+
const routes = useRoutes();
12+
13+
return (
14+
<div className={clsx(styles["profile-dropdown"], className)}>
15+
<Typography.Link href={routes.profile.index()}>
16+
<Icon name="icon-user" />
17+
</Typography.Link>
18+
</div>
19+
);
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ReactNode } from "react";
2+
3+
export type ProfileDropdownProps = {
4+
children?: ReactNode;
5+
className?: string;
6+
};

0 commit comments

Comments
 (0)