diff --git a/apps/web/package.json b/apps/web/package.json index 2f13fc74..772d2a47 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,18 +5,17 @@ "scripts": { "build": "next build", "dev": "next dev", - "postinstall": "prisma generate", "lint": "next lint", "start": "next start" }, "dependencies": { "@headlessui/react": "1.7.10", - "@heroicons/react": "^2.0.18", - "@prisma/client": "^4.14.0", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toolbar": "^1.0.4", - "@starknet-react/core": "^1.0.2", + "@starknet-react/chains": "^0.1.6", + "@starknet-react/core": "^2.2.4", "@t3-oss/env-nextjs": "^0.3.1", "@tanstack/react-query": "^4.29.7", "@trpc/client": "^10.26.0", @@ -24,17 +23,20 @@ "@trpc/react-query": "^10.26.0", "@trpc/server": "^10.26.0", "alchemy-sdk": "^2.8.3", - "design-system": "*", + "clsx": "^2.1.0", + "design-system": "workspace:*", + "framer-motion": "^11.0.3", "get-starknet-core": "^3.2.0", "next": "^13.4.2", "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", - "starknet": "^5.19.5", + "starknet": "^5.25.5", "superjson": "1.12.2", + "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^2.9.1", - "viem": "^1.10.7", - "wagmi": "^1.4.1", + "viem": "^2.7.6", + "wagmi": "^2.5.5", "zod": "^3.21.4" }, "devDependencies": { @@ -52,8 +54,7 @@ "postcss": "^8.4.21", "prettier": "^2.8.8", "prettier-plugin-tailwindcss": "^0.2.8", - "prisma": "^4.14.0", - "tailwind-config": "*", + "tailwind-config": "workspace:*", "tailwindcss": "^3.3.0", "typescript": "^5.3.3" }, diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico index a75b18e6..6bcbff98 100644 Binary files a/apps/web/public/favicon.ico and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/icons/arrow.svg b/apps/web/public/icons/arrow.svg new file mode 100644 index 00000000..de9442df --- /dev/null +++ b/apps/web/public/icons/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/logos/arkproject.svg b/apps/web/public/logos/arkproject.svg deleted file mode 100644 index 22e51fc8..00000000 --- a/apps/web/public/logos/arkproject.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/web/public/logos/dark/element.svg b/apps/web/public/logos/dark/element.svg new file mode 100644 index 00000000..12f55c3f --- /dev/null +++ b/apps/web/public/logos/dark/element.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/logos/dark/flex.png b/apps/web/public/logos/dark/flex.png new file mode 100644 index 00000000..4bb3a2d7 Binary files /dev/null and b/apps/web/public/logos/dark/flex.png differ diff --git a/apps/web/public/logos/dark/pyramid.png b/apps/web/public/logos/dark/pyramid.png new file mode 100644 index 00000000..b051965e Binary files /dev/null and b/apps/web/public/logos/dark/pyramid.png differ diff --git a/apps/web/public/logos/dark/unframed.svg b/apps/web/public/logos/dark/unframed.svg new file mode 100644 index 00000000..6a528888 --- /dev/null +++ b/apps/web/public/logos/dark/unframed.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/logos/dark/ventory.png b/apps/web/public/logos/dark/ventory.png new file mode 100644 index 00000000..196d8cab Binary files /dev/null and b/apps/web/public/logos/dark/ventory.png differ diff --git a/apps/web/public/logos/element.svg b/apps/web/public/logos/element.svg new file mode 100644 index 00000000..261345da --- /dev/null +++ b/apps/web/public/logos/element.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/logos/ethereum.svg b/apps/web/public/logos/ethereum.svg index 3355d3c4..fb542fb9 100644 --- a/apps/web/public/logos/ethereum.svg +++ b/apps/web/public/logos/ethereum.svg @@ -1,4 +1,8 @@ - - + + + + + + diff --git a/apps/web/public/logos/flex.png b/apps/web/public/logos/flex.png new file mode 100644 index 00000000..ec501997 Binary files /dev/null and b/apps/web/public/logos/flex.png differ diff --git a/apps/web/public/logos/pyramid.png b/apps/web/public/logos/pyramid.png new file mode 100644 index 00000000..e981e799 Binary files /dev/null and b/apps/web/public/logos/pyramid.png differ diff --git a/apps/web/public/logos/starknet.svg b/apps/web/public/logos/starknet.svg index 4c3fe8d9..6af0d2c0 100644 --- a/apps/web/public/logos/starknet.svg +++ b/apps/web/public/logos/starknet.svg @@ -1,9 +1,17 @@ - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/apps/web/public/logos/starkware.svg b/apps/web/public/logos/starkware.svg new file mode 100644 index 00000000..9016a974 --- /dev/null +++ b/apps/web/public/logos/starkware.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/logos/unframed.svg b/apps/web/public/logos/unframed.svg new file mode 100644 index 00000000..67cd06f3 --- /dev/null +++ b/apps/web/public/logos/unframed.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/logos/ventory.png b/apps/web/public/logos/ventory.png new file mode 100644 index 00000000..b2ac931d Binary files /dev/null and b/apps/web/public/logos/ventory.png differ diff --git a/apps/web/public/medias/bridging_quest_illustration.png b/apps/web/public/medias/bridging_quest_illustration.png new file mode 100644 index 00000000..02fbbd6b Binary files /dev/null and b/apps/web/public/medias/bridging_quest_illustration.png differ diff --git a/apps/web/public/medias/dark/bridge_animation.gif b/apps/web/public/medias/dark/bridge_animation.gif new file mode 100644 index 00000000..196fa398 Binary files /dev/null and b/apps/web/public/medias/dark/bridge_animation.gif differ diff --git a/apps/web/public/medias/dark/empty_card_1.png b/apps/web/public/medias/dark/empty_card_1.png deleted file mode 100644 index 783f11f1..00000000 Binary files a/apps/web/public/medias/dark/empty_card_1.png and /dev/null differ diff --git a/apps/web/public/medias/dark/empty_card_2.png b/apps/web/public/medias/dark/empty_card_2.png deleted file mode 100644 index 26a62e82..00000000 Binary files a/apps/web/public/medias/dark/empty_card_2.png and /dev/null differ diff --git a/apps/web/public/medias/dark/empty_card_3.png b/apps/web/public/medias/dark/empty_card_3.png deleted file mode 100644 index a29fb5c6..00000000 Binary files a/apps/web/public/medias/dark/empty_card_3.png and /dev/null differ diff --git a/apps/web/public/medias/dark/empty_card_4.png b/apps/web/public/medias/dark/empty_card_4.png deleted file mode 100644 index 60f62c71..00000000 Binary files a/apps/web/public/medias/dark/empty_card_4.png and /dev/null differ diff --git a/apps/web/public/medias/dark/empty_card_5.png b/apps/web/public/medias/dark/empty_card_5.png deleted file mode 100644 index 1992a600..00000000 Binary files a/apps/web/public/medias/dark/empty_card_5.png and /dev/null differ diff --git a/apps/web/public/medias/dark/empty_collection_card_1.png b/apps/web/public/medias/dark/empty_collection_card_1.png new file mode 100644 index 00000000..dea5661f Binary files /dev/null and b/apps/web/public/medias/dark/empty_collection_card_1.png differ diff --git a/apps/web/public/medias/dark/empty_collection_card_2.png b/apps/web/public/medias/dark/empty_collection_card_2.png new file mode 100644 index 00000000..4a1293fa Binary files /dev/null and b/apps/web/public/medias/dark/empty_collection_card_2.png differ diff --git a/apps/web/public/medias/dark/empty_collection_card_3.png b/apps/web/public/medias/dark/empty_collection_card_3.png new file mode 100644 index 00000000..83d33160 Binary files /dev/null and b/apps/web/public/medias/dark/empty_collection_card_3.png differ diff --git a/apps/web/public/medias/dark/empty_collection_card_4.png b/apps/web/public/medias/dark/empty_collection_card_4.png new file mode 100644 index 00000000..cf5cf345 Binary files /dev/null and b/apps/web/public/medias/dark/empty_collection_card_4.png differ diff --git a/apps/web/public/medias/dark/empty_collection_card_5.png b/apps/web/public/medias/dark/empty_collection_card_5.png new file mode 100644 index 00000000..5b99d17f Binary files /dev/null and b/apps/web/public/medias/dark/empty_collection_card_5.png differ diff --git a/apps/web/public/medias/dark/empty_token_card_1.png b/apps/web/public/medias/dark/empty_token_card_1.png new file mode 100644 index 00000000..808801ac Binary files /dev/null and b/apps/web/public/medias/dark/empty_token_card_1.png differ diff --git a/apps/web/public/medias/dark/empty_token_card_2.png b/apps/web/public/medias/dark/empty_token_card_2.png new file mode 100644 index 00000000..9dd86b7f Binary files /dev/null and b/apps/web/public/medias/dark/empty_token_card_2.png differ diff --git a/apps/web/public/medias/dark/empty_token_card_3.png b/apps/web/public/medias/dark/empty_token_card_3.png new file mode 100644 index 00000000..69558f7f Binary files /dev/null and b/apps/web/public/medias/dark/empty_token_card_3.png differ diff --git a/apps/web/public/medias/dark/empty_token_card_4.png b/apps/web/public/medias/dark/empty_token_card_4.png new file mode 100644 index 00000000..e6827644 Binary files /dev/null and b/apps/web/public/medias/dark/empty_token_card_4.png differ diff --git a/apps/web/public/medias/dark/empty_token_card_5.png b/apps/web/public/medias/dark/empty_token_card_5.png new file mode 100644 index 00000000..c0588a75 Binary files /dev/null and b/apps/web/public/medias/dark/empty_token_card_5.png differ diff --git a/apps/web/public/medias/dark/loading_card.png b/apps/web/public/medias/dark/loading_card.png deleted file mode 100644 index 3e5bc277..00000000 Binary files a/apps/web/public/medias/dark/loading_card.png and /dev/null differ diff --git a/apps/web/public/medias/dark/loading_token_card.png b/apps/web/public/medias/dark/loading_token_card.png new file mode 100644 index 00000000..5d532156 Binary files /dev/null and b/apps/web/public/medias/dark/loading_token_card.png differ diff --git a/apps/web/public/medias/dark/nft_selection_eth_empty.png b/apps/web/public/medias/dark/nft_selection_eth_empty.png new file mode 100644 index 00000000..6a022838 Binary files /dev/null and b/apps/web/public/medias/dark/nft_selection_eth_empty.png differ diff --git a/apps/web/public/medias/dark/nft_selection_starknet_empty.png b/apps/web/public/medias/dark/nft_selection_starknet_empty.png new file mode 100644 index 00000000..764d9801 Binary files /dev/null and b/apps/web/public/medias/dark/nft_selection_starknet_empty.png differ diff --git a/apps/web/public/medias/default_wallet.svg b/apps/web/public/medias/default_wallet.svg index 6efdbeec..aa70edd4 100644 --- a/apps/web/public/medias/default_wallet.svg +++ b/apps/web/public/medias/default_wallet.svg @@ -1,21 +1,21 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - + + diff --git a/apps/web/public/medias/empty_card_1.png b/apps/web/public/medias/empty_collection_card_1.png similarity index 100% rename from apps/web/public/medias/empty_card_1.png rename to apps/web/public/medias/empty_collection_card_1.png diff --git a/apps/web/public/medias/empty_card_2.png b/apps/web/public/medias/empty_collection_card_2.png similarity index 100% rename from apps/web/public/medias/empty_card_2.png rename to apps/web/public/medias/empty_collection_card_2.png diff --git a/apps/web/public/medias/empty_card_3.png b/apps/web/public/medias/empty_collection_card_3.png similarity index 100% rename from apps/web/public/medias/empty_card_3.png rename to apps/web/public/medias/empty_collection_card_3.png diff --git a/apps/web/public/medias/empty_card_4.png b/apps/web/public/medias/empty_collection_card_4.png similarity index 100% rename from apps/web/public/medias/empty_card_4.png rename to apps/web/public/medias/empty_collection_card_4.png diff --git a/apps/web/public/medias/empty_card_5.png b/apps/web/public/medias/empty_collection_card_5.png similarity index 100% rename from apps/web/public/medias/empty_card_5.png rename to apps/web/public/medias/empty_collection_card_5.png diff --git a/apps/web/public/medias/empty_token_card_1.png b/apps/web/public/medias/empty_token_card_1.png new file mode 100644 index 00000000..41bf7537 Binary files /dev/null and b/apps/web/public/medias/empty_token_card_1.png differ diff --git a/apps/web/public/medias/empty_token_card_2.png b/apps/web/public/medias/empty_token_card_2.png new file mode 100644 index 00000000..6ca8c93b Binary files /dev/null and b/apps/web/public/medias/empty_token_card_2.png differ diff --git a/apps/web/public/medias/empty_token_card_3.png b/apps/web/public/medias/empty_token_card_3.png new file mode 100644 index 00000000..f5e2a3bc Binary files /dev/null and b/apps/web/public/medias/empty_token_card_3.png differ diff --git a/apps/web/public/medias/empty_token_card_4.png b/apps/web/public/medias/empty_token_card_4.png new file mode 100644 index 00000000..2789a235 Binary files /dev/null and b/apps/web/public/medias/empty_token_card_4.png differ diff --git a/apps/web/public/medias/empty_token_card_5.png b/apps/web/public/medias/empty_token_card_5.png new file mode 100644 index 00000000..eea48291 Binary files /dev/null and b/apps/web/public/medias/empty_token_card_5.png differ diff --git a/apps/web/public/medias/ethereum_wallet.png b/apps/web/public/medias/ethereum_wallet.png index da3918e4..e6dd4260 100644 Binary files a/apps/web/public/medias/ethereum_wallet.png and b/apps/web/public/medias/ethereum_wallet.png differ diff --git a/apps/web/public/medias/everai_congrats.png b/apps/web/public/medias/everai_congrats.png new file mode 100644 index 00000000..3e296364 Binary files /dev/null and b/apps/web/public/medias/everai_congrats.png differ diff --git a/apps/web/public/medias/everai_samourai_1.png b/apps/web/public/medias/everai_samourai_1.png new file mode 100644 index 00000000..6d002a44 Binary files /dev/null and b/apps/web/public/medias/everai_samourai_1.png differ diff --git a/apps/web/public/medias/everai_samourai_2.png b/apps/web/public/medias/everai_samourai_2.png new file mode 100644 index 00000000..a18b6f93 Binary files /dev/null and b/apps/web/public/medias/everai_samourai_2.png differ diff --git a/apps/web/public/medias/faq_banner.svg b/apps/web/public/medias/faq_banner.svg new file mode 100644 index 00000000..e590098b --- /dev/null +++ b/apps/web/public/medias/faq_banner.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/medias/home_background.png b/apps/web/public/medias/home_background.png new file mode 100644 index 00000000..1d7cc353 Binary files /dev/null and b/apps/web/public/medias/home_background.png differ diff --git a/apps/web/public/medias/loading_card.png b/apps/web/public/medias/loading_card.png deleted file mode 100644 index 0f859656..00000000 Binary files a/apps/web/public/medias/loading_card.png and /dev/null differ diff --git a/apps/web/public/medias/loading_token_card.png b/apps/web/public/medias/loading_token_card.png new file mode 100644 index 00000000..49388ce7 Binary files /dev/null and b/apps/web/public/medias/loading_token_card.png differ diff --git a/apps/web/public/medias/nft_selection_empty.png b/apps/web/public/medias/nft_selection_eth_empty.png similarity index 100% rename from apps/web/public/medias/nft_selection_empty.png rename to apps/web/public/medias/nft_selection_eth_empty.png diff --git a/apps/web/public/medias/nft_selection_starknet_empty.png b/apps/web/public/medias/nft_selection_starknet_empty.png new file mode 100644 index 00000000..f51168d1 Binary files /dev/null and b/apps/web/public/medias/nft_selection_starknet_empty.png differ diff --git a/apps/web/public/medias/quest_banner.png b/apps/web/public/medias/quest_banner.png new file mode 100644 index 00000000..2dade24a Binary files /dev/null and b/apps/web/public/medias/quest_banner.png differ diff --git a/apps/web/public/medias/starknet_wallet.png b/apps/web/public/medias/starknet_wallet.png index 5518027e..2bd8d9fc 100644 Binary files a/apps/web/public/medias/starknet_wallet.png and b/apps/web/public/medias/starknet_wallet.png differ diff --git a/apps/web/public/medias/vault.svg b/apps/web/public/medias/vault.svg index 1ca92490..579ae62c 100644 --- a/apps/web/public/medias/vault.svg +++ b/apps/web/public/medias/vault.svg @@ -1,67 +1,66 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/medias/wallet_default.png b/apps/web/public/medias/wallet_default.png new file mode 100644 index 00000000..ae8bac61 Binary files /dev/null and b/apps/web/public/medias/wallet_default.png differ diff --git a/apps/web/src/app/(routes)/bridge/[address]/TokenList.tsx b/apps/web/src/app/(routes)/bridge/[address]/TokenList.tsx new file mode 100644 index 00000000..fbf1f17a --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/[address]/TokenList.tsx @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +"use client"; + +import { Button, Typography } from "design-system"; + +import InfiniteScrollButton from "~/app/_components/InfiniteScrollButton"; +import NftCard from "~/app/_components/NftCard/NftCard"; +import NftsLoadingState from "~/app/_components/NftsLoadingState"; +import TokenNftsEmptyState from "~/app/_components/TokenNftsEmptyState"; +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import useInfiniteEthereumNfts from "~/app/_hooks/useInfiniteEthereumNfts"; +import useInfiniteStarknetNfts from "~/app/_hooks/useInfiniteStarknetNfts"; +import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected"; +import { api } from "~/utils/api"; + +import useNftSelection, { MAX_SELECTED_ITEMS } from "../_hooks/useNftSelection"; + +interface TokenListProps { + nftContractAddress: string; +} + +export default function TokenList({ nftContractAddress }: TokenListProps) { + const { sourceChain } = useCurrentChain(); + + const { + deselectAllNfts, + isNftSelected, + selectBatchNfts, + selectedCollectionAddress, + toggleNftSelection, + totalSelectedNfts, + } = useNftSelection(); + + const isFullyConnected = useIsFullyConnected(); + + const { + data: l1NftsData, + fetchNextPage: fetchNextl1NftsPage, + hasNextPage: hasNextl1NftsPage, + isFetchingNextPage: isFetchingNextl1NftsPage, + totalCount: l1NftsTotalCount, + } = useInfiniteEthereumNfts({ contractAddress: nftContractAddress }); + + const { + data: l2NftsData, + fetchNextPage: fetchNextl2NftsPage, + hasNextPage: hasNextl2NftsPage, + isFetchingNextPage: isFetchingNextl2NftsPage, + totalCount: l2NftsTotalCount, + } = useInfiniteStarknetNfts({ contractAddress: nftContractAddress }); + + const { data: l1CollectionInfo } = api.l1Nfts.getCollectionInfo.useQuery( + { contractAddress: nftContractAddress }, + { enabled: sourceChain === "Ethereum" } + ); + + const { data: l2CollectionInfo } = api.l2Nfts.getCollectionInfo.useQuery( + { contractAddress: nftContractAddress }, + { enabled: sourceChain === "Starknet" } + ); + + // TODO @YohanTz: Extract to a hook + const collectionData = + sourceChain === "Ethereum" ? l1CollectionInfo : l2CollectionInfo; + const nftsData = sourceChain === "Ethereum" ? l1NftsData : l2NftsData; + const fetchNextPage = + sourceChain === "Ethereum" ? fetchNextl1NftsPage : fetchNextl2NftsPage; + const hasNextPage = + sourceChain === "Ethereum" ? hasNextl1NftsPage : hasNextl2NftsPage; + const isFetchingNextPage = + sourceChain === "Ethereum" + ? isFetchingNextl1NftsPage + : isFetchingNextl2NftsPage; + const totalCount = + sourceChain === "Ethereum" ? l1NftsTotalCount : l2NftsTotalCount; + + if (nftsData?.pages[0]?.ownedNfts.length === 0 || !isFullyConnected) { + return ; + } + + if (nftsData === undefined) { + return ; + } + + const hasMoreThan100Nfts = + nftsData.pages.length > 1 || (nftsData.pages.length === 1 && hasNextPage); + + const isAllSelected = + (totalSelectedNfts === MAX_SELECTED_ITEMS || + totalSelectedNfts === nftsData.pages[0]?.ownedNfts.length) && + nftContractAddress === selectedCollectionAddress; + + return ( +
+
+
+ + {collectionData ? collectionData.name : ""} Collection + + + {totalCount} + {totalCount ?? 0 > 1 ? " Nfts" : " Nft"} + +
+ {isAllSelected ? ( + + ) : ( + + )} +
+ +
+ {nftsData.pages.map((page) => { + return page.ownedNfts.map((ownedNft) => { + const isSelected = isNftSelected( + ownedNft.tokenId, + ownedNft.contractAddress + ); + + return ( + + toggleNftSelection(ownedNft.tokenId, ownedNft.contractAddress) + } + cardType="nft" + chain={sourceChain} + disabled={isAllSelected && !isSelected} + image={ownedNft.image} + isSelected={isSelected} + key={ownedNft.tokenId} + title={ownedNft.name} + /> + ); + }); + })} +
+ fetchNextPage()} + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + /> +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/[address]/page.tsx b/apps/web/src/app/(routes)/bridge/[address]/page.tsx new file mode 100644 index 00000000..2e8edb18 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/[address]/page.tsx @@ -0,0 +1,40 @@ +import { Typography } from "design-system"; +import Link from "next/link"; + +import TokenList from "./TokenList"; + +interface PageProps { + params: { address: string }; +} + +export default function Page({ params: { address } }: PageProps) { + return ( + <> + {/* TODO @YohanTz: Refacto to be a variant in the Button component */} +
+ + {/* TODO @YohanTz: Export svg to icons file */} + + + + Back + +
+ + + ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/BridgingQuestBanner.tsx b/apps/web/src/app/(routes)/bridge/_components/BridgingQuestBanner.tsx new file mode 100644 index 00000000..cc351726 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/BridgingQuestBanner.tsx @@ -0,0 +1,49 @@ +"use client"; + +import clsx from "clsx"; +import { Typography } from "design-system"; +import Image from "next/image"; + +interface BridgingQuestBanner { + className?: string; +} + +export default function BridgingQuestBanner({ + className, +}: BridgingQuestBanner) { + return ( +
+
+ + Everai Bridging Quests + + + Bridge Everai NFTs and enter the competition by completing the first + ArkProject quests. + + + View quests + +
+ Bridging Quest +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/CollectionGrid.tsx b/apps/web/src/app/(routes)/bridge/_components/CollectionGrid.tsx new file mode 100644 index 00000000..86664b5e --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/CollectionGrid.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; + +import CollectionNftsEmptyState from "~/app/_components/CollectionNftsEmptyState"; +import ConditionalWrapper from "~/app/_components/ConditionalWrapper"; +import NftCard from "~/app/_components/NftCard/NftCard"; +import NftsLoadingState from "~/app/_components/NftsLoadingState"; +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected"; +import { type Collection } from "~/server/api/types"; + +import useNftSelection from "../_hooks/useNftSelection"; + +interface CollectionGridProps { + nftCollectionPages?: Array<{ + collections: Array; + totalCount: number; + }>; +} + +/* + * TODO @YohanTz: Take time to optimize the lists with React.memo etc. + */ +export default function CollectionGrid({ + nftCollectionPages, +}: CollectionGridProps) { + const { sourceChain } = useCurrentChain(); + const { selectedCollectionAddress } = useNftSelection(); + const isFullyConnected = useIsFullyConnected(); + + if (nftCollectionPages?.[0]?.collections.length === 0 || !isFullyConnected) { + return ; + } + + if (nftCollectionPages === undefined) { + return ; + } + + return ( +
+ {nftCollectionPages?.map((nftCollectionPage) => { + return nftCollectionPage.collections.map((nftCollection) => { + return ( + + nftCollection.isBridgeable ? ( + + {children} + + ) : ( +
{children}
+ ) + } + key={nftCollection.contractAddress} + > + {}} + title={nftCollection.name} + /> +
+ ); + }); + })} +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/CollectionHeader.tsx b/apps/web/src/app/(routes)/bridge/_components/CollectionHeader.tsx new file mode 100644 index 00000000..ba79c323 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/CollectionHeader.tsx @@ -0,0 +1,39 @@ +import { Typography } from "design-system"; + +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected"; + +interface CollectionHeaderProps { + collectionTotalCount?: number; +} +export default function CollectionHeader({ + collectionTotalCount, +}: CollectionHeaderProps) { + const { sourceChain, targetChain } = useCurrentChain(); + const isFullyConnected = useIsFullyConnected(); + + return ( +
+
+ + Collections on {sourceChain} + + {collectionTotalCount !== undefined && ( + + {isFullyConnected ? collectionTotalCount : 0} + + )} +
+ + {collectionTotalCount === undefined && isFullyConnected + ? "Loading collections in progress..." + : collectionTotalCount === 0 || !isFullyConnected + ? `It looks like you have no nfts collection on ${sourceChain}...` + : `Select the assets you want to transfer to ${targetChain}`} + +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/Collections.tsx b/apps/web/src/app/(routes)/bridge/_components/Collections.tsx new file mode 100644 index 00000000..5c4ad7fa --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/Collections.tsx @@ -0,0 +1,32 @@ +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import useInfiniteEthereumCollections from "~/app/_hooks/useInfiniteEthereumCollections"; +import useInfiniteStarknetCollections from "~/app/_hooks/useInfiniteStarknetCollections"; + +import CollectionGrid from "./CollectionGrid"; +import CollectionHeader from "./CollectionHeader"; + +export default function Collections() { + const { sourceChain } = useCurrentChain(); + + const { data: l1CollectionsData, totalCount: l1CollectionsTotalCount } = + useInfiniteEthereumCollections(); + const { data: l2CollectionsData, totalCount: l2CollectionsTotalCount } = + useInfiniteStarknetCollections(); + + const pages = + sourceChain === "Ethereum" + ? l1CollectionsData?.pages + : l2CollectionsData?.pages; + + const totalCount = + sourceChain === "Ethereum" + ? l1CollectionsTotalCount + : l2CollectionsTotalCount; + + return ( + <> + + + + ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/NftTransferSummary.tsx b/apps/web/src/app/(routes)/bridge/_components/NftTransferSummary.tsx deleted file mode 100644 index 175bbff7..00000000 --- a/apps/web/src/app/(routes)/bridge/_components/NftTransferSummary.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { - Button, - Drawer, - IconButton, - Modal, - Notification, - Typography, -} from "design-system"; -import Image from "next/image"; -import { redirect } from "next/navigation"; -import { useEffect, useState } from "react"; - -import useCurrentChain from "~/app/_hooks/useCurrentChain"; - -import useNftSelection from "../_hooks/useNftSelection"; -import useTransferNftsFromChain from "../_hooks/useTransferNfts"; -import WalletsTransferSummary from "./WalletsTransferSummary"; - -function TransferAction() { - const { sourceChain, targetChain } = useCurrentChain(); - const { numberOfSelectedNfts } = useNftSelection(); - - const { - approveForAll, - depositTokens, - isApproveLoading, - isApprovedForAll, - isDepositLoading, - isDepositSuccess, - } = useTransferNftsFromChain(sourceChain); - - useEffect(() => { - if (isDepositSuccess) { - redirect("/lounge"); - } - }, [isDepositSuccess]); - - return isApprovedForAll ? ( - <> - - - - - - - - - - - - } - className="mt-8" - variant="gas_fee" - > - Gas fees are free, handed by Ark Project! - - - {isDepositLoading && ( - Bridge loading animation - )} - - ) : ( - <> - {numberOfSelectedNfts > 0 && ( - - - - - - - You must approve the selection of your assets before confirming the - migration. Each collection will require a signature via your wallet. - - )} - - - ); -} - -function TransferSummary() { - // TODO @YohanTz: Support both sides - const { deselectNft, selectedNfts } = useNftSelection(); - - return ( - <> - - Your assets to transfer - - - - - {selectedNfts.length > 0 ? ( - - {selectedNfts.length} Nfts selected - - ) : ( -
- no nft selected nft image - - No Nfts selected yet... -
- Select a collection to start. -
-
- )} - - {/* TODO @YohanTz: Always show scroll bar to indicate that there is more content to view (with Radix ScrollArea ?) */} - {selectedNfts.length > 0 && ( -
- {selectedNfts.map((selectedNft) => { - return ( -
-
- {selectedNft?.image ? ( - {selectedNft?.title - ) : ( - //
- <> - empty Nft image - empty Nft image - - )} -
- - {selectedNft?.collectionName} - - - {selectedNft?.title.length ?? 0 > 0 - ? selectedNft?.title - : selectedNft?.tokenId} - -
-
- - - - } - onClick={() => deselectNft(selectedNft?.id ?? "")} - /> -
- ); - })} -
- )} - - - ); -} - -export default function TransferSummaryContainer() { - const [showMobileSummary, setShowMobileSummary] = useState(false); - const { numberOfSelectedNfts } = useNftSelection(); - - return ( - <> - - - - {numberOfSelectedNfts > 0 && ( - - {showMobileSummary ? ( - - ) : ( - <> - - Your assets to transfer - -
- - {numberOfSelectedNfts}{" "} - {numberOfSelectedNfts > 1 ? "Nfts" : "Nft"} selected - - - {/* */} -
- - )} -
- )} - - ); -} diff --git a/apps/web/src/app/(routes)/bridge/_components/SmallBridgingQuestBanner.tsx b/apps/web/src/app/(routes)/bridge/_components/SmallBridgingQuestBanner.tsx new file mode 100644 index 00000000..1e79e917 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/SmallBridgingQuestBanner.tsx @@ -0,0 +1,40 @@ +"use client"; + +import clsx from "clsx"; +import { Typography } from "design-system"; +import Image from "next/image"; + +interface BridgingQuestBanner { + className?: string; +} + +export default function SmallBridgingQuestBanner({ + className, +}: BridgingQuestBanner) { + return ( + +
+ + Bridge your Everai NFTs and complete your first ArkProject quests. + +
+ View quests +
+
+ Bridging Quest +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/TargetChainButton.tsx b/apps/web/src/app/(routes)/bridge/_components/TargetChainButton.tsx index 5c1a8670..ab1e9f5a 100644 --- a/apps/web/src/app/(routes)/bridge/_components/TargetChainButton.tsx +++ b/apps/web/src/app/(routes)/bridge/_components/TargetChainButton.tsx @@ -1,3 +1,5 @@ +import Link from "next/link"; + import useCurrentChain from "~/app/_hooks/useCurrentChain"; import { useIsSSR } from "~/app/_hooks/useIsSSR"; @@ -9,26 +11,28 @@ export default function TargetChainButton() { return (
{!isSSR && ( - + + + + + )}
); diff --git a/apps/web/src/app/(routes)/bridge/_components/TargetChainSwitch.tsx b/apps/web/src/app/(routes)/bridge/_components/TargetChainSwitch.tsx index a1480550..c73f7fb6 100644 --- a/apps/web/src/app/(routes)/bridge/_components/TargetChainSwitch.tsx +++ b/apps/web/src/app/(routes)/bridge/_components/TargetChainSwitch.tsx @@ -11,20 +11,19 @@ export default function TargetChainSwitch() { return (
-
+
{`${sourceChain}
From @@ -35,12 +34,12 @@ export default function TargetChainSwitch() { -
+
To @@ -49,9 +48,10 @@ export default function TargetChainSwitch() { {`${targetChain}
diff --git a/apps/web/src/app/(routes)/bridge/_components/TokenList.tsx b/apps/web/src/app/(routes)/bridge/_components/TokenList.tsx deleted file mode 100644 index 07f4c025..00000000 --- a/apps/web/src/app/(routes)/bridge/_components/TokenList.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { Button, Typography } from "design-system"; - -import NftsEmptyState from "~/app/_components/NftsEmptyState"; -import NftsLoadingState from "~/app/_components/NftsLoadingState"; -import useCurrentChain from "~/app/_hooks/useCurrentChain"; -import { type Nft } from "~/server/api/routers/nfts"; - -import NftCard from "../../../_components/NftCard/NftCard"; -import useNftSelection from "../_hooks/useNftSelection"; - -interface CollectionGridProps { - selectCollection: (collectionName: null | string) => void; -} - -function CollectionGrid({ selectCollection }: CollectionGridProps) { - const { lastSelectedCollectionName, nfts } = useNftSelection(); - const { sourceChain } = useCurrentChain(); - - if (nfts === undefined) { - return ; - } - - if (nfts.raw.length === 0) { - return ; - } - - return ( -
- {Object.entries(nfts.byCollection).map( - ([collectionName, collectionNfts]) => { - return ( - selectCollection(collectionName)} - title={collectionName} - /> - ); - } - )} -
- ); -} - -function CollectionList({ selectCollection }: CollectionGridProps) { - const { sourceChain, targetChain } = useCurrentChain(); - const { nfts } = useNftSelection(); - - return ( - <> -
-
- - Collections on {sourceChain} - - {nfts === undefined ? null : ( - - {Object.keys(nfts?.byCollection)?.length ?? 0} - - )} -
- - {nfts === undefined - ? "Loading collections in progress..." - : nfts.raw.length === 0 - ? `It looks like you have no nfts collection on ${sourceChain}...` - : `Select the assets you want to transfer to ${targetChain}`} - -
- - - ); -} - -interface CollectionTokenListProps { - allCollectionSelected: boolean; - selectCollection: (collectionName: null | string) => void; - selectedCollection: Array; - selectedCollectionName: null | string; - toggleNftSelection: (nftId: string) => void; - toggleSelectAll: () => void; -} - -function CollectionTokenList({ - allCollectionSelected, - selectCollection, - selectedCollection, - selectedCollectionName, - toggleNftSelection, - toggleSelectAll, -}: CollectionTokenListProps) { - const { sourceChain } = useCurrentChain(); - - const { isSelected } = useNftSelection(); - - return ( -
- {/* TODO @YohanTz: Refacto to be a variant in the Button component */} - -
-
- - {selectedCollectionName} Collection - - - {selectedCollection.length} - {selectedCollection.length > 1 ? " Nfts" : " Nft"} - -
- -
- -
- {selectedCollection.map((nft) => { - return ( - toggleNftSelection(nft.id)} - title={nft.title.length > 0 ? nft.title : nft.tokenId} - /> - ); - })} -
-
- ); -} - -/* - * TODO @YohanTz: Take time to optimize the lists with React.memo etc. - */ -export default function TokenList() { - // TODO @YohanTz: Refactor this part of the hook - const { - allCollectionSelected, - selectCollection, - selectedCollection, - selectedCollectionName, - toggleNftSelection, - toggleSelectAll, - } = useNftSelection(); - - return ( -
- {selectedCollection.length === 0 ? ( - - ) : ( - - )} -
- ); -} diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferEthereumNftsAction.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferEthereumNftsAction.tsx new file mode 100644 index 00000000..767da178 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/TransferEthereumNftsAction.tsx @@ -0,0 +1,139 @@ +import clsx from "clsx"; +import { Button, Typography } from "design-system"; + +import useEthereumCollectionApproval from "../_hooks/useEthereumCollectionApproval"; +import useEthereumNftDeposit from "../_hooks/useEthereumNftDeposit"; +import useNftSelection from "../_hooks/useNftSelection"; + +function ApproveNfts() { + const { approveForAll, isApproveLoading, isSigning } = + useEthereumCollectionApproval(); + + const disabled = isApproveLoading || isSigning; + + return ( + <> + + + + + + + You must approve the selection of your assets before confirming the + migration. Each collection will require a signature via your wallet. + + + + + ); +} + +function TransferNfts() { + const { depositTokens, isSigning } = useEthereumNftDeposit(); + const { totalSelectedNfts } = useNftSelection(); + + const disabled = totalSelectedNfts === 0 || isSigning; + + return ( + + ); +} + +export default function TransferEthereumNftsAction() { + const { totalSelectedNfts } = useNftSelection(); + const { isApprovedForAll } = useEthereumCollectionApproval(); + + return isApprovedForAll || totalSelectedNfts === 0 ? ( + + ) : ( + + ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferNftsAction.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferNftsAction.tsx new file mode 100644 index 00000000..a674b92a --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/TransferNftsAction.tsx @@ -0,0 +1,14 @@ +import useCurrentChain from "~/app/_hooks/useCurrentChain"; + +import TransferEthereumNftsAction from "./TransferEthereumNftsAction"; +import TransferStarknetNftsAction from "./TransferStarknetNftsAction"; + +export default function TransferNftsAction() { + const { sourceChain } = useCurrentChain(); + + if (sourceChain === "Ethereum") { + return ; + } + + return ; +} diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx new file mode 100644 index 00000000..baae2e8d --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/TransferNftsList.tsx @@ -0,0 +1,119 @@ +import { IconButton, Typography } from "design-system"; + +import Media from "~/app/_components/Media"; +import useAccountFromChain from "~/app/_hooks/useAccountFromChain"; +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import { api } from "~/utils/api"; + +import useNftSelection from "../_hooks/useNftSelection"; + +export default function TransferNftsList() { + const { deselectNft, selectedCollectionAddress, selectedTokenIds } = + useNftSelection(); + + const { sourceChain } = useCurrentChain(); + const { address } = useAccountFromChain(sourceChain); + + const { data: l1SelectedNfts } = api.l1Nfts.getNftMetadataBatch.useQuery( + { + contractAddress: selectedCollectionAddress ?? "", + tokenIds: selectedTokenIds, + }, + { + enabled: sourceChain === "Ethereum", + keepPreviousData: true, + } + ); + + const { data: l2SelectedNfts } = api.l2Nfts.getNftMetadataBatch.useQuery( + { + contractAddress: selectedCollectionAddress ?? "", + ownerAddress: address ?? "", + tokenIds: selectedTokenIds, + }, + { + enabled: sourceChain === "Starknet", + keepPreviousData: true, + } + ); + + const selectedNfts = + sourceChain === "Ethereum" ? l1SelectedNfts : l2SelectedNfts; + + if (selectedNfts === undefined) { + return null; + } + + return ( +
+ {selectedNfts.map((selectedNft) => { + return ( +
+
+ {selectedNft?.image ? ( + + ) : ( + <> + + + + )} +
+ + {selectedNft.collectionName} + + + {selectedNft.tokenName} + +
+
+ + + + } + onClick={() => + deselectNft( + selectedNft.tokenId, + selectedCollectionAddress ?? "" + ) + } + className="flex-shrink-0" + /> +
+ ); + })} +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferNftsSummary.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferNftsSummary.tsx new file mode 100644 index 00000000..c265f873 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/TransferNftsSummary.tsx @@ -0,0 +1,232 @@ +import { useAccount as useStarknetAccount } from "@starknet-react/core"; +import clsx from "clsx"; +import { Button, Drawer, SideModal, Typography } from "design-system"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { useAccount as useEthereumAccount } from "wagmi"; + +import { useConnectModals } from "~/app/_components/WalletModals/WalletModalsContext"; +import useCurrentChain from "~/app/_hooks/useCurrentChain"; +import useIsFullyConnected from "~/app/_hooks/useIsFullyConnected"; + +import useNftSelection from "../_hooks/useNftSelection"; +import SmallBridgingQuestBanner from "./SmallBridgingQuestBanner"; +import TransferNftsAction from "./TransferNftsAction"; +import TransferNftsList from "./TransferNftsList"; +import TransferNftsWalletSummary from "./TransferNftsWalletSummary"; + +function NoNftsImage() { + const { sourceChain } = useCurrentChain(); + + if (sourceChain === "Starknet") { + return ( + <> + no nft selected nft image + no nft selected nft image + + ); + } + + return ( + <> + no nft selected nft image + no nft selected nft image + + ); +} + +function TransferNftsNotConnected() { + const { address: ethereumAddress } = useEthereumAccount(); + const { address: starknetAddress } = useStarknetAccount(); + + const { + toggleConnectEthereumWalletModal, + toggleConnectStarknetWalletModal, + toggleConnectWalletsModal, + } = useConnectModals(); + + if (ethereumAddress === undefined && starknetAddress === undefined) { + return ( + <> + {`Wallets`} + + + ); + } else if (ethereumAddress === undefined) { + return ( + <> + {`Ethereum + + + ); + } else if (starknetAddress === undefined) { + return ( + <> + {`Starknet + + + + ); + } + + return null; +} + +function TransferSummary() { + const { totalSelectedNfts } = useNftSelection(); + const pathname = usePathname(); + + const hasSelectedNfts = totalSelectedNfts > 0; + const showBridgingQuestBanner = pathname === "/bridge"; + const isFullyConnected = useIsFullyConnected(); + + return ( + <> + + Your assets to transfer + + + + + {isFullyConnected ? ( + <> + {hasSelectedNfts ? ( + <> + + {totalSelectedNfts} Nfts selected + + + + ) : ( +
+ + + No Nfts selected yet... +
+ For now you can only select Everai collection! +
+
+ )} + + + ) : ( + + )} + + {showBridgingQuestBanner && } + + ); +} + +export default function TransferSummaryContainer() { + const [showMobileSummary, setShowMobileSummary] = useState(false); + const { totalSelectedNfts } = useNftSelection(); + + return ( + <> + + + + {totalSelectedNfts > 0 && ( + + {showMobileSummary ? ( + + ) : ( + <> + + Your assets to transfer + +
+ + {totalSelectedNfts} {totalSelectedNfts > 1 ? "Nfts" : "Nft"}{" "} + selected + + + {/* */} +
+ + )} +
+ )} + + ); +} diff --git a/apps/web/src/app/(routes)/bridge/_components/WalletsTransferSummary.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferNftsWalletSummary.tsx similarity index 60% rename from apps/web/src/app/(routes)/bridge/_components/WalletsTransferSummary.tsx rename to apps/web/src/app/(routes)/bridge/_components/TransferNftsWalletSummary.tsx index cb047e39..5d189398 100644 --- a/apps/web/src/app/(routes)/bridge/_components/WalletsTransferSummary.tsx +++ b/apps/web/src/app/(routes)/bridge/_components/TransferNftsWalletSummary.tsx @@ -8,20 +8,18 @@ import useCurrentChain from "~/app/_hooks/useCurrentChain"; import { type Chain } from "../../../_types"; import TargetChainButton from "./TargetChainButton"; -export default function WalletsTransferSummary() { +export default function TransferNftsWalletSummary() { const { sourceChain, targetChain } = useCurrentChain(); - const { address: ethereumAddress } = - useEthereumAccount(); - const { address: starknetAddress } = - useStarknetAccount(); + const { address: ethereumAddress } = useEthereumAccount(); + const { address: starknetAddress } = useStarknetAccount(); // TODO @YohanTz: Hook wrapper around wagmi and starknet-react const shortEthereumAddress = useMemo( () => ethereumAddress ? `${ethereumAddress.slice(0, 6)}...${ethereumAddress.slice(-4)}` - : "", + : undefined, [ethereumAddress] ); @@ -29,38 +27,38 @@ export default function WalletsTransferSummary() { () => starknetAddress ? `${starknetAddress.slice(0, 6)}...${starknetAddress.slice(-4)}` - : "", + : undefined, [starknetAddress] ); - const shortAddressByChain: Record = { + const shortAddressByChain: Record = { Ethereum: shortEthereumAddress, Starknet: shortStarknetAddress, }; return ( -
+
From wallet - - {shortAddressByChain[sourceChain]} + + {shortAddressByChain[sourceChain] ?? "Not connected"}
To wallet - - {shortAddressByChain[targetChain]} + + {shortAddressByChain[targetChain] ?? "Not connected"}
diff --git a/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx b/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx new file mode 100644 index 00000000..01ca4bc2 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_components/TransferStarknetNftsAction.tsx @@ -0,0 +1,38 @@ +import clsx from "clsx"; +import { Button, Typography } from "design-system"; + +import useNftSelection from "../_hooks/useNftSelection"; +import useTransferStarknetNfts from "../_hooks/useTransferStarknetNfts"; + +export default function TransferStarknetNftsAction() { + const { totalSelectedNfts } = useNftSelection(); + const { depositTokens, isSigning } = useTransferStarknetNfts(); + + const disabled = totalSelectedNfts === 0 || isSigning; + + return ( + + ); +} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx new file mode 100644 index 00000000..5f4b5b22 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumCollectionApproval.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect } from "react"; +import { erc721Abi } from "viem"; +import { + useBlockNumber, + useAccount as useEthereumAccount, + useReadContract, + useWaitForTransactionReceipt, + useWriteContract, +} from "wagmi"; + +import useNftSelection from "./useNftSelection"; + +export default function useEthereumCollectionApproval() { + const { selectedCollectionAddress, totalSelectedNfts } = useNftSelection(); + + const { address: ethereumAddress } = useEthereumAccount(); + + const { data: isApprovedForAll, refetch } = useReadContract({ + abi: erc721Abi, + address: selectedCollectionAddress as `0x${string}`, + args: [ + ethereumAddress ?? "0xa", + process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, + ], + functionName: "isApprovedForAll", + query: { + enabled: totalSelectedNfts > 0, + }, + }); + + const { + data: approveHash, + isLoading: isSigning, + writeContract: writeContractApprove, + } = useWriteContract(); + + function approveForAll() { + writeContractApprove({ + abi: erc721Abi, + address: selectedCollectionAddress as `0x${string}`, + args: [process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, true], + functionName: "setApprovalForAll", + }); + } + + const { isLoading: isApproveLoading } = useWaitForTransactionReceipt({ + hash: approveHash, + }); + + const { data: blockNumber } = useBlockNumber({ watch: true }); + + useEffect(() => { + void refetch(); + }, [blockNumber, refetch]); + + return { + approveForAll: () => approveForAll(), + isApproveLoading: isApproveLoading && approveHash !== undefined, + isApprovedForAll, + isSigning, + }; +} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx new file mode 100644 index 00000000..f3df9bec --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/_hooks/useEthereumNftDeposit.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useAccount as useStarknetAccount } from "@starknet-react/core"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { parseGwei } from "viem"; +import { useWriteContract } from "wagmi"; + +import useNftSelection from "./useNftSelection"; + +export default function useEthereumNftDeposit() { + const { deselectAllNfts, selectedCollectionAddress, selectedTokenIds } = + useNftSelection(); + + const { address: starknetAddress } = useStarknetAccount(); + + const { + data: depositTransactionHash, + isLoading, + writeContract: writeContractDeposit, + } = useWriteContract(); + + const router = useRouter(); + + function depositTokens() { + writeContractDeposit({ + abi: [ + { + inputs: [ + { + internalType: "uint256", + name: "salt", + type: "uint256", + }, + { + internalType: "address", + name: "collectionL1", + type: "address", + }, + { + internalType: "snaddress", + name: "ownerL2", + type: "uint256", + }, + { + internalType: "uint256[]", + name: "ids", + type: "uint256[]", + }, + { + internalType: "bool", + name: "useAutoBurn", + type: "bool", + }, + ], + name: "depositTokens", + outputs: [], + stateMutability: "payable", + type: "function", + }, + ], + address: process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, + args: [ + // TODO @YohanTz: Get the proper request hash from ? + Date.now(), + selectedCollectionAddress as `0x${string}`, + starknetAddress, + selectedTokenIds, + false, + ], + functionName: "depositTokens", + // TODO @YohanTz: Get needed gas from ? + value: parseGwei("40000"), + }); + } + + useEffect(() => { + if (depositTransactionHash !== undefined) { + void router.push(`lounge/${depositTransactionHash}?from=ethereum`); + } + }, [depositTransactionHash, deselectAllNfts, router]); + + return { + depositTokens: () => depositTokens(), + depositTransactionHash, + isSigning: isLoading && depositTransactionHash === undefined, + }; +} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts b/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts index f6a25be0..e879ddb5 100644 --- a/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts +++ b/apps/web/src/app/(routes)/bridge/_hooks/useNftSelection.ts @@ -1,190 +1,168 @@ -import { useEffect, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useLocalStorage } from "usehooks-ts"; import useAccountFromChain from "~/app/_hooks/useAccountFromChain"; import useCurrentChain from "~/app/_hooks/useCurrentChain"; -import { api } from "~/utils/api"; +import { type Nft } from "~/server/api/types"; + +export const MAX_SELECTED_ITEMS = 100; export default function useNftSelection() { const { sourceChain } = useCurrentChain(); - const { address } = useAccountFromChain(sourceChain); - - const [selectedNftIdsByAddress, setSelectedNftIdsByAddress] = useLocalStorage< - Record<`0x${string}`, Array> - >("selectedNftIdsByAddress", {}); - - const [ - lastSelectedCollectionNameByAddress, - setLastSelectedCollectionNameByAddress, - ] = useLocalStorage>( - "lastSelectedCollectionNameByAddress", - {} + const { address: userAddress } = useAccountFromChain(sourceChain); + + const [selectedTokensByUserAddress, setSelectedTokensByUserAddress] = + useLocalStorage< + Record< + `0x${string}`, + { collectionAddress: string; tokenIds: Array } | null + > + >("selectedTokensByUserAddress", {}); + + const { selectedCollectionAddress, selectedTokenIds } = useMemo( + () => ({ + selectedCollectionAddress: userAddress + ? selectedTokensByUserAddress[userAddress]?.collectionAddress + : undefined, + selectedTokenIds: userAddress + ? selectedTokensByUserAddress[userAddress]?.tokenIds ?? [] + : [], + }), + [selectedTokensByUserAddress, userAddress] ); - // Change to use contract address - const [selectedCollectionName, setSelectedCollectionName] = useState< - null | string - >(null); - - const { data: l1Nfts } = api.nfts.getL1NftsByCollection.useQuery( - { - address: address ?? "", - }, - { - enabled: address !== undefined && sourceChain === "Ethereum", - } + const totalSelectedNfts = useMemo( + () => selectedTokenIds.length, + [selectedTokenIds] ); - const { data: l2Nfts } = api.nfts.getL2NftsByCollection.useQuery( - { - address: address ?? "", + + const isNftSelected = useCallback( + (tokenId: string, collectionAddress: string) => { + return ( + selectedTokenIds.includes(tokenId) && + collectionAddress === selectedCollectionAddress + ); }, - { - enabled: address !== undefined && sourceChain === "Starknet", - } + [selectedCollectionAddress, selectedTokenIds] ); - const nfts = sourceChain === "Ethereum" ? l1Nfts : l2Nfts; - - const selectedCollection = selectedCollectionName - ? nfts?.byCollection[selectedCollectionName] ?? [] - : []; - - /** - * array.filter() is used because we need to clean the nft ids that are still in the local storage - * but that are not in the user wallet anymore - */ - const selectedNftIds = address - ? selectedNftIdsByAddress[address]?.filter( - (nftId) => nfts?.raw.find((rawNft) => rawNft.id === nftId) !== undefined - ) ?? [] - : []; - - const numberOfSelectedNfts = selectedNftIds.length; - - const lastSelectedCollectionName = - address && selectedNftIds.length > 0 - ? lastSelectedCollectionNameByAddress[address] - : undefined; - - // TODO @YohanTz: Directly search in the collection - const selectedNfts = selectedNftIds - .map((selectedNftId) => nfts?.raw.find((nft) => nft.id === selectedNftId)) - .filter((nft) => nft !== undefined); - - const allCollectionSelected = - selectedCollection.length === selectedNftIds.length; - - // @YohanTz: Refacto to remove the need of useEffect - useEffect(() => { - setSelectedCollectionName(null); - }, [sourceChain]); - - function deselectNft(nftId: string) { - if (address === undefined || !selectedNftIds.includes(nftId)) { - return null; - } - - if (selectedNftIds.length === 1) { - setLastSelectedCollectionNameByAddress({ - ...lastSelectedCollectionNameByAddress, - [address]: null, - }); + function selectNft(tokenId: string, collectionAddress: string) { + if ( + isNftSelected(tokenId, collectionAddress) || + userAddress === undefined + ) { + return; } - setSelectedNftIdsByAddress({ - ...selectedNftIdsByAddress, - [address]: selectedNftIds.filter( - (selectedNftId) => selectedNftId !== nftId - ), - }); - } - - function selectNft(nftId: string) { - if (address === undefined) { - return null; + if ( + totalSelectedNfts === MAX_SELECTED_ITEMS && + collectionAddress === selectedCollectionAddress + ) { + // TODO @YohanTz: Trigger toast here + return; } if ( - selectedCollectionName !== lastSelectedCollectionNameByAddress[address] + collectionAddress !== + selectedTokensByUserAddress[userAddress]?.collectionAddress ) { - setSelectedNftIdsByAddress({ - ...selectedNftIdsByAddress, - [address]: [nftId], - }); - setLastSelectedCollectionNameByAddress({ - ...lastSelectedCollectionNameByAddress, - [address]: selectedCollectionName, - }); + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: { collectionAddress, tokenIds: [tokenId] }, + })); return; } - setSelectedNftIdsByAddress({ - ...selectedNftIdsByAddress, - [address]: [...selectedNftIds, nftId], - }); + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: { + collectionAddress, + tokenIds: [...selectedTokenIds, tokenId], + }, + })); } - function toggleNftSelection(nftId: string) { - if (address === undefined) { - return null; - } + const deselectNft = useCallback( + (tokenId: string, collectionAddress: string) => { + if ( + !isNftSelected(tokenId, collectionAddress) || + userAddress === undefined + ) { + return; + } + + if (selectedTokenIds.length === 1) { + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: undefined, + })); + return; + } + + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: { + collectionAddress, + tokenIds: selectedTokenIds.filter( + (selectedTokenId) => selectedTokenId !== tokenId + ), + }, + })); + }, + [ + isNftSelected, + selectedTokenIds, + setSelectedTokensByUserAddress, + userAddress, + ] + ); - if (selectedNftIds.includes(nftId)) { - deselectNft(nftId); + function selectBatchNfts(nfts: Array) { + if (nfts.length === 0 || userAddress === undefined) { return; } - selectNft(nftId); + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: { + collectionAddress: nfts[0]?.contractAddress, + tokenIds: nfts.map((nft) => nft.tokenId).slice(0, MAX_SELECTED_ITEMS), + }, + })); } - function toggleSelectAll() { - if (address === undefined || nfts === undefined) { + const deselectAllNfts = useCallback(() => { + if (userAddress === undefined) { return; } - if (allCollectionSelected) { - setSelectedNftIdsByAddress({ - ...selectedNftIdsByAddress, - [address]: [], - }); - setLastSelectedCollectionNameByAddress({ - ...lastSelectedCollectionNameByAddress, - [address]: null, - }); + setSelectedTokensByUserAddress((previousValue) => ({ + ...previousValue, + [userAddress]: undefined, + })); + }, [setSelectedTokensByUserAddress, userAddress]); + + function toggleNftSelection(tokenId: string, collectionAddress: string) { + if (userAddress === undefined) { return; } - setSelectedNftIdsByAddress({ - ...selectedNftIdsByAddress, - [address]: selectedCollection.map((nft) => nft.id), - }); - setLastSelectedCollectionNameByAddress({ - ...lastSelectedCollectionNameByAddress, - [address]: selectedCollectionName, - }); - } - function selectCollection(collectionName: null | string) { - setSelectedCollectionName(collectionName); - } + if (isNftSelected(tokenId, collectionAddress)) { + deselectNft(tokenId, collectionAddress); + return; + } - function isSelected(nftId: string) { - return selectedNftIds.includes(nftId); + selectNft(tokenId, collectionAddress); } return { - allCollectionSelected, + deselectAllNfts, deselectNft, - isSelected, - lastSelectedCollectionName, - nfts, - numberOfSelectedNfts, - selectCollection, - selectNft, - selectedCollection, - selectedCollectionName, - selectedNftIds, - selectedNfts, + isNftSelected, + selectBatchNfts, + selectedCollectionAddress, + selectedTokenIds, toggleNftSelection, - toggleSelectAll, + totalSelectedNfts, }; } diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts deleted file mode 100644 index c983acf9..00000000 --- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferEthereumNfts.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { useAccount as useStarknetAccount } from "@starknet-react/core"; -import { parseGwei } from "viem"; -import { - erc721ABI, - useContractRead, - useContractWrite, - useAccount as useEthereumAccount, - useWaitForTransaction, -} from "wagmi"; - -import useNftSelection from "./useNftSelection"; - -export default function useTransferEthereumNfts() { - const { numberOfSelectedNfts, selectedNfts } = useNftSelection(); - - const { address: ethereumAddress } = useEthereumAccount(); - const { address: starknetAddress } = useStarknetAccount(); - - const { data: isApprovedForAll } = useContractRead({ - abi: erc721ABI, - address: selectedNfts[0]?.collectionContractAddress as `0x${string}`, - args: [ - ethereumAddress ?? "0xtest", - process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, - ], - enabled: numberOfSelectedNfts > 0, - functionName: "isApprovedForAll", - watch: true, - }); - - const { data: approveData, write: approveForAll } = useContractWrite({ - abi: erc721ABI, - address: selectedNfts[0]?.collectionContractAddress as `0x${string}`, - args: [process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, true], - functionName: "setApprovalForAll", - }); - - const { isLoading: isApproveLoading } = useWaitForTransaction({ - hash: approveData?.hash, - }); - - const { data: depositData, write: depositTokens } = useContractWrite({ - abi: [ - { - inputs: [ - { - internalType: "uint256", - name: "salt", - type: "uint256", - }, - { - internalType: "address", - name: "collectionL1", - type: "address", - }, - { - internalType: "snaddress", - name: "ownerL2", - type: "uint256", - }, - { - internalType: "uint256[]", - name: "ids", - type: "uint256[]", - }, - { - internalType: "bool", - name: "useAutoBurn", - type: "bool", - }, - ], - name: "depositTokens", - outputs: [], - stateMutability: "payable", - type: "function", - }, - ], - address: process.env.NEXT_PUBLIC_L1_BRIDGE_ADDRESS as `0x${string}`, - args: [ - // TODO @YohanTz: Get the proper request hash from ? - Date.now(), - selectedNfts[0]?.collectionContractAddress as `0x${string}`, - starknetAddress, - selectedNfts.map((selectedNft) => selectedNft?.tokenId), - false, - ], - functionName: "depositTokens", - // TODO @YohanTz: Get needed gas from ? - value: parseGwei("40000"), - }); - - // Use isSuccess from useWaitForTransaction once fixed... or once we do not rely on a public provider ? - const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = - useWaitForTransaction({ - hash: depositData?.hash, - }); - - return { - approveForAll: () => approveForAll(), - depositTokens: () => depositTokens(), - isApproveLoading, - isApprovedForAll, - isDepositLoading, - isDepositSuccess, - }; -} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts deleted file mode 100644 index ad68d251..00000000 --- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferNfts.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type Chain } from "~/app/_types"; - -import useTransferEthereumNfts from "./useTransferEthereumNfts"; -import useTransferStarknetNfts from "./useTransferStarknetNfts"; - -export default function useTransferNftsFromChain(chain: Chain) { - const transferEthereumNfts = useTransferEthereumNfts(); - const transferStarknetNfts = useTransferStarknetNfts(); - - return chain === "Ethereum" ? transferEthereumNfts : transferStarknetNfts; -} diff --git a/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts b/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts index 5e22104c..fe74e24f 100644 --- a/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts +++ b/apps/web/src/app/(routes)/bridge/_hooks/useTransferStarknetNfts.ts @@ -1,10 +1,10 @@ import { useContract, useContractRead, - useContractWrite, useAccount as useStarknetAccount, - useWaitForTransaction, } from "@starknet-react/core"; +import { useRouter } from "next/navigation"; +import { useCallback, useMemo, useState } from "react"; import { CallData } from "starknet"; import { useAccount as useEthereumAccount } from "wagmi"; @@ -13,12 +13,13 @@ import useNftSelection from "./useNftSelection"; const L2_BRIDGE_ADDRESS = process.env.NEXT_PUBLIC_L2_BRIDGE_ADDRESS || ""; export default function useTransferStarknetNfts() { - const { selectedNfts } = useNftSelection(); + const [isSigning, setIsSigning] = useState(false); + const { selectedCollectionAddress, selectedTokenIds } = useNftSelection(); const { address: ethereumAddress } = useEthereumAccount(); - const { address: starknetAddress } = useStarknetAccount(); + const { account: starknetAccount, address: starknetAddress } = + useStarknetAccount(); - // TODO @YohanTz: Cast type const { data: isApprovedForAll } = useContractRead({ abi: [ { @@ -42,7 +43,7 @@ export default function useTransferStarknetNfts() { type: "function", }, ], - address: selectedNfts[0]?.collectionContractAddress ?? "", + address: selectedCollectionAddress ?? "", args: [starknetAddress ?? "0xtest", L2_BRIDGE_ADDRESS], functionName: "is_approved_for_all", watch: true, @@ -86,61 +87,69 @@ export default function useTransferStarknetNfts() { address: L2_BRIDGE_ADDRESS, }); - const { data: approveData, write: approveForAll } = useContractWrite({ - calls: [ - { - calldata: [L2_BRIDGE_ADDRESS, 1], - contractAddress: selectedNfts[0]?.collectionContractAddress ?? "", - entrypoint: "set_approval_for_all", - }, - ], - }); + const getDepositCalldata = useCallback(() => { + if ( + bridgeContract?.abi !== undefined && + ethereumAddress !== undefined && + selectedCollectionAddress !== undefined + ) { + const depositCallData = new CallData(bridgeContract?.abi); + return depositCallData.compile("deposit_tokens", { + collection_l2: selectedCollectionAddress, + owner_l1: ethereumAddress, + salt: Date.now(), + token_ids: selectedTokenIds, + use_deposit_burn_auto: false, + use_withdraw_auto: false, + }); + } + }, [ + bridgeContract?.abi, + ethereumAddress, + selectedCollectionAddress, + selectedTokenIds, + ]); - const { isLoading: isApproveLoading } = useWaitForTransaction({ - hash: approveData?.transaction_hash, - }); + const depositCalls = useMemo(() => { + const approveCall = { + calldata: [L2_BRIDGE_ADDRESS, 1], + contractAddress: selectedCollectionAddress ?? "", + entrypoint: "set_approval_for_all", + }; - // TODO @YohanTz: Refacto - let depositCallData = undefined; - if ( - bridgeContract?.abi !== undefined && - ethereumAddress !== undefined && - selectedNfts[0] !== undefined - ) { - depositCallData = new CallData(bridgeContract?.abi); - depositCallData = depositCallData.compile("deposit_tokens", { - collection_l2: selectedNfts[0]?.collectionContractAddress ?? "", - owner_l1: ethereumAddress, - salt: Date.now(), - token_ids: selectedNfts.map((selectedNft) => selectedNft?.tokenId), - use_deposit_burn_auto: false, - use_withdraw_auto: true, - }); - } + const depositCall = { + calldata: getDepositCalldata(), + contractAddress: L2_BRIDGE_ADDRESS, + entrypoint: "deposit_tokens", + }; - const { data: depositData, write: depositTokens } = useContractWrite({ - calls: [ - { - calldata: depositCallData, - contractAddress: L2_BRIDGE_ADDRESS, - entrypoint: "deposit_tokens", - }, - ], - }); + if (!isApprovedForAll) { + return [approveCall, depositCall]; + } + + return [depositCall]; + }, [getDepositCalldata, isApprovedForAll, selectedCollectionAddress]); + + async function depositTokens() { + setIsSigning(true); + try { + // await writeAsync(); + const depositData = await starknetAccount?.execute(depositCalls); + if (depositData !== undefined) { + router.push(`lounge/${depositData.transaction_hash}`); + setIsSigning(false); + } + } catch (error) { + console.log(error); + setIsSigning(false); + } + } - const { isLoading: isDepositLoading, isSuccess: isDepositSuccess } = - useWaitForTransaction({ - hash: depositData?.transaction_hash, - }); + const router = useRouter(); return { - approveForAll: () => approveForAll(), depositTokens: () => depositTokens(), - isApproveLoading: - isApproveLoading && approveData?.transaction_hash !== undefined, - isApprovedForAll, - isDepositLoading: - isDepositLoading && depositData?.transaction_hash !== undefined, - isDepositSuccess, + // isSigning: isSigning && depositData?.transaction_hash !== undefined, + isSigning, }; } diff --git a/apps/web/src/app/(routes)/bridge/layout.tsx b/apps/web/src/app/(routes)/bridge/layout.tsx new file mode 100644 index 00000000..fb3f30c4 --- /dev/null +++ b/apps/web/src/app/(routes)/bridge/layout.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { Typography } from "design-system"; + +import MainPageContainer from "../../_components/MainPageContainer"; +import TargetChainSwitch from "./_components/TargetChainSwitch"; +import TransferNftsSummary from "./_components/TransferNftsSummary"; + +// TODO @YohanTz: Refactor when the UX is finalized +export default function Page({ children }: { children: React.ReactNode }) { + return ( +
+ + + {/* Where do you want to move +
+ your digital goods? */} + Where do you want to move
+ your Everai? +
+ + + + {children} +
+ + +
+ ); +} diff --git a/apps/web/src/app/(routes)/bridge/page.tsx b/apps/web/src/app/(routes)/bridge/page.tsx index c1449201..12690789 100644 --- a/apps/web/src/app/(routes)/bridge/page.tsx +++ b/apps/web/src/app/(routes)/bridge/page.tsx @@ -1,29 +1,7 @@ "use client"; -import { Typography } from "design-system"; +import Collections from "./_components/Collections"; -import MainPageContainer from "../../_components/MainPageContainer"; -import NftTransferSummary from "./_components/NftTransferSummary"; -import TargetChainSwitch from "./_components/TargetChainSwitch"; -import TokenList from "./_components/TokenList"; - -// TODO @YohanTz: Refactor when the UX is finalized export default function Page() { - return ( -
- - - Where do you want to move -
- your digital goods? -
- - - - -
- - -
- ); + return ; } diff --git a/apps/web/src/app/(routes)/faq/_components/Banner.tsx b/apps/web/src/app/(routes)/faq/_components/Banner.tsx new file mode 100644 index 00000000..6ffdf5ac --- /dev/null +++ b/apps/web/src/app/(routes)/faq/_components/Banner.tsx @@ -0,0 +1,28 @@ +import clsx from "clsx"; +import { Typography } from "design-system"; +import Image from "next/image"; + +interface BannerProps { + className?: string; +} +export default function Banner({ className }: BannerProps) { + return ( +
+ FAQ + + Frequently Asked +
+ Questions +
+
+ ); +} diff --git a/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx b/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx new file mode 100644 index 00000000..752a07a6 --- /dev/null +++ b/apps/web/src/app/(routes)/faq/_components/FaqEntries.tsx @@ -0,0 +1,120 @@ +import clsx from "clsx"; + +import FaqEntry from "./FaqEntry"; + +interface FaqEntriesProps { + className?: string; +} + +export default function FaqEntries({ className }: FaqEntriesProps) { + return ( +
+ + The ArkProject Bridge, developed by Screenshot Labs, allows users to + bridge NFTs (ERC-721) between Ethereum (L1) and Starknet (L2). + + + {`The ArkProject Bridge currently supports the bridging of the Everai NFT + collection exclusively. Holders of Everai NFTs can seamlessly transfer + their assets from Ethereum (L1) to Starknet (L2) and vice versa, + pioneering the integration of NFTs into the next generation of + blockchain technology. Don't own an Everai yet? `} + Buy one{" "} + + here + {" "} + and join the bridging fun! + + + In order to bridge NFTs from Ethereum (L1) to Starknet (L2) via the + ArkProject Bridge, you will need to set up a Starknet wallet (eg.{" "} + + Argent + {" "} + or{" "} + + Braavos + + {`) to which you will send the NFTs. Then, you will need to + connect both your Ethereum (L1) wallet and Starknet (L2) wallet to + ArkProject bridge, and define the NFTs you'd like to send.`} + + + The ArkProject NFT Bridge enables two-way transfers. You can bridge your + Everai NFTs from Ethereum (L1) to Starknet (L2) and also from Starknet + (L2) back to Ethereum (L1). This flexibility allows NFT holders to + leverage the benefits of both L1 and L2 technologies whenever they see + fit. + + + Yes, bridging transactions require the payment of gas fees. When + transferring NFTs from Ethereum (L1) to Starknet (L2) or vice versa, you + will be responsible for the gas fees associated with these transactions + on the respective networks. These fees contribute to the processing and + security of your transactions on the blockchain. + + + {`In the unlikely event of a transfer error during the bridging process, + you will have the option to click the "Return To Ethereum (L1)" button. + This safety feature is designed to ensure that your assets are not lost + and can be securely retrieved, providing peace of mind during the + transfer process.`} + + + Should you have any questions or require assistance, our support team is + ready to help you. You can reach us at{" "} + + support@arkproject.dev + + . Our team is committed to providing timely and helpful responses to + ensure a smooth and enjoyable experience with the ArkProject NFT Bridge. + + + Transfers typically complete within a few minutes to a few hours, + depending on network congestion. We strive to ensure that your bridging + experience is as efficient as possible. + + + When you transfer NFTs from Ethereum to Starknet, it all happens in one + single transaction. However, if you want to take the bridge from + Starknet back to Ethereum, you need two separate transactions. Users + often forget the second step, as it is relevant only once your NFTs are + actually moved to L1, sometimes hours after you initiated your + transaction. +
+
+ First, you initiate the transfer on L2 with your Starknet wallet, then + you need to wait until the block containing the transaction has been + proved and verified by the Starknet verifier smart contract on Ethereum + L1. This can take a few hours. +
+
+ Then you will need to connect again with your Ethereum wallet to + ArkProject Bridge and issue a withdraw transaction, withdrawing the NFTs + from the Ethereum side of the bridge. +
+
+ {`Until you do this, the NFTs will remain in the L2 side of the bridge and + won't go through to your L1 wallet.`} +
+
+ ); +} diff --git a/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx b/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx new file mode 100644 index 00000000..f372de78 --- /dev/null +++ b/apps/web/src/app/(routes)/faq/_components/FaqEntry.tsx @@ -0,0 +1,41 @@ +"use client"; + +import * as Collapsible from "@radix-ui/react-collapsible"; +import { MinusIcon, PlusIcon, Typography } from "design-system"; +import { type PropsWithChildren, useState } from "react"; + +interface FaqEntryProps { + title: string; +} + +export default function FaqEntry({ + children, + title, +}: PropsWithChildren) { + const [open, setOpen] = useState(false); + + return ( + +
+
+ {title} + + + +
+ + + + {children} + + +
+
+ ); +} diff --git a/apps/web/src/app/(routes)/faq/page.tsx b/apps/web/src/app/(routes)/faq/page.tsx new file mode 100644 index 00000000..e7b35b1d --- /dev/null +++ b/apps/web/src/app/(routes)/faq/page.tsx @@ -0,0 +1,19 @@ +import Footer from "~/app/_components/Footer"; +import MainPageContainer from "~/app/_components/MainPageContainer"; + +import Banner from "./_components/Banner"; +import FaqEntries from "./_components/FaqEntries"; + +export default function FaqPage() { + return ( + <> +
+ + + + +
+