diff --git a/.env.example b/.env.example index 1340f397..ddabee2b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,40 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=50db9e195634356bb8ab0375a9f07607 +NEXT_PUBLIC_ALCHEMY_ETHEREUM_HTTP= +NEXT_PUBLIC_ALCHEMY_ETHEREUM_KEY= NEXT_PUBLIC_ALCHEMY_SEPOLIA_HTTP= NEXT_PUBLIC_ALCHEMY_SEPOLIA_KEY= +NEXT_PUBLIC_ALCHEMY_POLYGON_HTTP= +NEXT_PUBLIC_ALCHEMY_POLYGON_KEY= NEXT_PUBLIC_ALCHEMY_MUMBAI_HTTP= NEXT_PUBLIC_ALCHEMY_MUMBAI_KEY= -NEXT_PUBLIC_ALCHEMY_ETHEREUM_HTTP= +NEXT_PUBLIC_ALCHEMY_OPTIMISM_HTTP= +NEXT_PUBLIC_ALCHEMY_OPTIMISM_KEY= +NEXT_PUBLIC_ALCHEMY_OPGOERLI_HTTP= +NEXT_PUBLIC_ALCHEMY_OPGOERLI_KEY= +NEXT_PUBLIC_ALCHEMY_OPSEPOLIA_HTTP= +NEXT_PUBLIC_ALCHEMY_OPSEPOLIA_KEY= +NEXT_PUBLIC_ALCHEMY_AVALANCHE_HTTP= +NEXT_PUBLIC_ALCHEMY_AVALANCHE_KEY= +NEXT_PUBLIC_ALCHEMY_FUJI_HTTP= +NEXT_PUBLIC_ALCHEMY_FUJI_KEY= +NEXT_PUBLIC_ALCHEMY_BASE_HTTP= +NEXT_PUBLIC_ALCHEMY_BASE_KEY= +NEXT_PUBLIC_ALCHEMY_BASEGOERLI_HTTP= +NEXT_PUBLIC_ALCHEMY_BASEGOERLI_KEY= +NEXT_PUBLIC_ALCHEMY_BNBTESTNET_HTTP= +NEXT_PUBLIC_ALCHEMY_BNBTESTNET_KEY= +NEXT_PUBLIC_ALCHEMY_ARBITRUMONE_HTTP= +NEXT_PUBLIC_ALCHEMY_ARBITRUMONE_KEY= +NEXT_PUBLIC_ALCHEMY_ARBITRUMSEPOLIA_HTTP= +NEXT_PUBLIC_ALCHEMY_ARBITRUMSEPOLIA_KEY= + +# Subgraph endpoint production query + +# ENDPOINT_URL=https://gateway.testnet.thegraph.com/api/[api-key]/subgraphs/id/EynHJVht9r6xhaZEPCyLYLd4EqBq8msf4jTrw1Vwg8ZV + + +# Subgraph endpoint production query + +NEXT_PUBLIC_ENDPOINT_URL=https://api.studio.thegraph.com/query/57887/swaplace-subgraph-sepolia/version/latest + +NEXT_PUBLIC_SUBGRAPH_AUTH_KEY=3b2048f02febad918a35bbafe78b2115 \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 40c7174d..b2bb4788 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -11,12 +11,12 @@ name: ESLint on: push: - branches: [ "main" ] + branches: ["main"] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '16 6 * * 3' + - cron: "16 6 * * 3" jobs: eslint: diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..98e57f6f --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,5 @@ +trailingComma: "all" +singleQuote: false +printWidth: 80 +tabWidth: 2 +semi: true diff --git a/CODEOWNERS b/CODEOWNERS index b0269319..b0c0ffc7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,3 +1,3 @@ # Default owners for everything in the repo -* @FrancoAguzzi @heronlancellot @eduramme +- @FrancoAguzzi @heronlancellot @eduramme @0xneves diff --git a/components/01-atoms/ConfirmSwapModal.tsx b/components/01-atoms/ConfirmSwapModal.tsx deleted file mode 100644 index 5a488d14..00000000 --- a/components/01-atoms/ConfirmSwapModal.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { - NftCard, - NftCardActionType, - SwapContext, - SwapIcon, - TransactionResultModal, -} from "."; -import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; -import { SWAPLACE_SMART_CONTRACT_ADDRESS } from "@/lib/client/constants"; -import { Dialog, Transition } from "@headlessui/react"; -import { Fragment, useContext, useEffect, useState } from "react"; -import { useNetwork, useWalletClient } from "wagmi"; -import toast from "react-hot-toast"; - -interface ConfirmSwapModalProps { - open: boolean; - onClose: () => void; -} - -enum CreateOfferStatus { - "CREATE_OFFER" = "CREATE_OFFER", - "WAITING_WALLET_APPROVAL" = "WAITING_WALLET_APPROVAL", -} - -export enum TransactionResult { - "LOADING" = "LOADING", - "SUCCESS" = "SUCCESS", - "FAILURE" = "FAILURE", -} - -export const ConfirmSwapModal = ({ open, onClose }: ConfirmSwapModalProps) => { - const { authenticatedUserAddress } = useAuthenticatedUser(); - const { nftInputUser, nftAuthUser, validatedAddressToSwap } = - useContext(SwapContext); - - const [createOfferStatus, setCreateOfferStatus] = useState( - CreateOfferStatus.CREATE_OFFER - ); - const [transactionResult, displayTransactionResultModal] = - useState(null); - - const { chain } = useNetwork(); - const { data: walletClient } = useWalletClient(); - - useEffect(() => { - if (!open) { - setCreateOfferStatus(CreateOfferStatus.CREATE_OFFER); - } - }, [open]); - - if (!authenticatedUserAddress?.address || !nftInputUser || !nftAuthUser) { - onClose(); - return null; - } - - const createOffer = async () => { - const swaplaceContractForCurrentChain = - chain && SWAPLACE_SMART_CONTRACT_ADDRESS[chain?.id]; - - if (swaplaceContractForCurrentChain && walletClient) { - // const farFarInTheFuture = getTimestamp(chain.id); - - setCreateOfferStatus(CreateOfferStatus.WAITING_WALLET_APPROVAL); - walletClient - .signMessage({ - message: "Swaplace dApps wants to create a Swap Offer!", - }) - .then(() => { - setCreateOfferStatus(CreateOfferStatus.CREATE_OFFER); - displayTransactionResultModal(TransactionResult.LOADING); - setTimeout(() => { - displayTransactionResultModal(TransactionResult.SUCCESS); - }, 10000); - }) - .catch((error) => { - toast.error("Message signature failed"); - setCreateOfferStatus(CreateOfferStatus.CREATE_OFFER); - displayTransactionResultModal(TransactionResult.FAILURE); - console.error(error); - }); - - // makeSwap( - // swaplaceContractForCurrentChain, - // authenticatedUserAddress.address, - // validatedAddressToSwap, - // destinyChain, - // farFarInTheFuture, - // nftAuthUser, - // nftInputUser, - // chain.id - // ) - // .then((data) => { - // console.log("data", data); - // }) - // .catch((error) => { - // console.log("error", error); - // }); - } else { - throw new Error( - "Could not find a Swaplace contract for the current chain" - ); - } - }; - - return ( - <> - -
- - - - - - Confirm Swap Offer creation - - - Please confirm the data below is correct: - -
-
-
You are offering
-
- -
-
- -
-
You are asking for
-
- -
-
-
- - Tip: you can click on the NFT card
to copy its metadata to - your clipboard! -
- - - Are you sure you want to create this Swap Offer? - - -
-
-
- - - - ); -}; diff --git a/components/01-atoms/ConnectWallet.tsx b/components/01-atoms/ConnectWallet.tsx index 4be484ff..77944f21 100644 --- a/components/01-atoms/ConnectWallet.tsx +++ b/components/01-atoms/ConnectWallet.tsx @@ -1,11 +1,13 @@ +import { WalletIcon } from "@/components/01-atoms"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useRouter } from "next/router"; interface IConnectWallet { customStyle?: string; + walletIcon?: boolean; } -export const ConnectWallet = ({ customStyle }: IConnectWallet) => { +export const ConnectWallet = ({ customStyle, walletIcon }: IConnectWallet) => { const router = useRouter(); return ( @@ -44,10 +46,10 @@ export const ConnectWallet = ({ customStyle }: IConnectWallet) => { return ( ); } @@ -65,7 +67,7 @@ export const ConnectWallet = ({ customStyle }: IConnectWallet) => {
+ ); +}; diff --git a/components/01-atoms/ENSAvatar.tsx b/components/01-atoms/ENSAvatar.tsx new file mode 100644 index 00000000..214169c1 --- /dev/null +++ b/components/01-atoms/ENSAvatar.tsx @@ -0,0 +1,78 @@ +import { LoadingIndicator } from "."; +import { + ENSAvatarQueryStatus, + useEnsData, +} from "@/lib/client/hooks/useENSData"; +import { EthereumAddress } from "@/lib/shared/types"; +import BoringAvatar from "boring-avatars"; +import cc from "classcat"; + +enum ENSAvatarSize { + SMALL = "small", + MEDIUM = "medium", +} + +type Size = ENSAvatarSize | "small" | "medium"; + +const ENSAvatarClassName = { + [ENSAvatarSize.SMALL]: "ens-avatar-small", + [ENSAvatarSize.MEDIUM]: "ens-avatar-medium", +}; + +interface ENSAvatarProps { + avatarENSAddress: EthereumAddress; + size?: Size; +} + +export const ENSAvatar = ({ + avatarENSAddress, + size = ENSAvatarSize.MEDIUM, +}: ENSAvatarProps) => { + const { avatarQueryStatus, avatarSrc } = useEnsData({ + ensAddress: avatarENSAddress, + }); + + return ( +
+ {avatarQueryStatus === ENSAvatarQueryStatus.LOADING ? ( +
+ +
+ ) : avatarQueryStatus === ENSAvatarQueryStatus.ERROR ? ( +
+ +
+ ) : /* + Below condition will always be true since we only have 3 possible values + for avatarQueryStatus, being the third one ENSAvatarQueryStatus.SUCCESS: + + When the query is successful, avatarSrc will be defined + */ + avatarSrc ? ( + {`ENS + ) : null} +
+ ); +}; diff --git a/components/01-atoms/EmptyNftsCards.tsx b/components/01-atoms/EmptyNftsCards.tsx new file mode 100644 index 00000000..cb67f5ef --- /dev/null +++ b/components/01-atoms/EmptyNftsCards.tsx @@ -0,0 +1,42 @@ +import { useScreenSize } from "@/lib/client/hooks/useScreenSize"; + +export const EmptyNftsCards = ( + tokenArrayLength: number, + ismobileTotalSquares: number, + isWideScreenTotalSquares: number, + isDesktopTotalSquares: number, + isTabletTotalSquares: number, +) => { + const { isDesktop, isTablet, isWideScreen, isMobile } = useScreenSize(); + + let totalSquares = 0; + let totalSquaresX = 0; // Token quantity in X axis + + // We are getting X count as the LCM to fill the rows with empty cards correctly. + isMobile + ? ((totalSquares = ismobileTotalSquares), + ismobileTotalSquares == 4 ? (totalSquaresX = 4) : (totalSquaresX = 3)) + : isWideScreen + ? ((totalSquares = isWideScreenTotalSquares), + isWideScreenTotalSquares == 8 ? (totalSquaresX = 4) : (totalSquaresX = 6)) + : isDesktop + ? ((totalSquares = isDesktopTotalSquares), (totalSquaresX = 6)) + : isTablet && ((totalSquares = isTabletTotalSquares), (totalSquaresX = 6)); + + const spareTokensX = tokenArrayLength % totalSquaresX; + const emptySquaresCountX = spareTokensX ? totalSquaresX - spareTokensX : 0; + + const spareTokens = totalSquares - tokenArrayLength; + const emptySquaresCount = + emptySquaresCountX < spareTokens + ? Math.max(spareTokens, 0) + : emptySquaresCountX; + + const emptySquares = Array.from({ length: emptySquaresCount }, (_, index) => ( + <> +
+ + )); + + return emptySquares; +}; diff --git a/components/01-atoms/LoadingIndicator.tsx b/components/01-atoms/LoadingIndicator.tsx index 1107a5db..d432d0ae 100644 --- a/components/01-atoms/LoadingIndicator.tsx +++ b/components/01-atoms/LoadingIndicator.tsx @@ -1,7 +1,5 @@ import React from "react"; -const LoadingIndicator = () => ( +export const LoadingIndicator = () => (
); - -export default LoadingIndicator; diff --git a/components/01-atoms/NftsCardApprovedList.tsx b/components/01-atoms/NftsCardApprovedList.tsx new file mode 100644 index 00000000..c4b105b9 --- /dev/null +++ b/components/01-atoms/NftsCardApprovedList.tsx @@ -0,0 +1,164 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ADDRESS_ZERO, + NFT, + SWAPLACE_SMART_CONTRACT_ADDRESS, +} from "@/lib/client/constants"; +import { SwapContext } from "@/components/01-atoms"; +import { + NftCard, + NftCardActionType, + NftCardStyleType, +} from "@/components/02-molecules"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { updateNftsToSwapApprovalStatus } from "@/lib/client/swap-utils"; +import { IApproveSwap } from "@/lib/client/blockchain-data"; +import { approveSwap } from "@/lib/service/approveSwap"; +import cc from "classcat"; +import { useContext, useEffect } from "react"; +import toast from "react-hot-toast"; +import { useNetwork, useWalletClient } from "wagmi"; +import { hexToNumber } from "viem"; + +export const NftsCardApprovedList = () => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + const { chain } = useNetwork(); + const { data: walletClient } = useWalletClient(); + + const { + nftAuthUser, + authedUserSelectedNftsApprovalStatus, + setAuthedUserNftsApprovalStatus, + setAllSelectedNftsAreApproved, + allSelectedNftsApproved, + } = useContext(SwapContext); + + useEffect(() => { + const fetchApprove = async () => { + await updateNftsToSwapApprovalStatus( + nftAuthUser, + setAuthedUserNftsApprovalStatus, + setAllSelectedNftsAreApproved, + ); + }; + fetchApprove(); + }, [nftAuthUser, allSelectedNftsApproved]); + + if (!authenticatedUserAddress?.address) { + return null; + } + let chainId: number; + const handleApprove = async (nft: NFT) => { + if (typeof chain?.id != "undefined") { + chainId = chain?.id; + } + + const swapData: IApproveSwap = { + walletClient: walletClient, + spender: SWAPLACE_SMART_CONTRACT_ADDRESS[chainId] as `0x${string}`, + tokenContractAddress: nft.contract?.address, + amountOrId: BigInt(hexToNumber(nft.id?.tokenId)), + }; + + try { + const transactionReceipt = await approveSwap(swapData); + if (transactionReceipt != undefined) { + toast.success("Approval successfully"); + return transactionReceipt; + } else { + toast.error("Approval Failed"); + } + } catch (error) { + toast.error("Approval Rejected"); + console.error(error); + } + }; + + const validateApprovalTokens = (arraynftApproval: any[]) => { + const isValidApproved = !arraynftApproval.some((token) => { + return token.approved != SWAPLACE_SMART_CONTRACT_ADDRESS[chainId]; + }); + + setAllSelectedNftsAreApproved(isValidApproved); + }; + + const approveNftForSwapping = async (nft: NFT, index: number) => { + if (typeof chain?.id != "undefined") { + chainId = chain?.id; + } + if ( + authedUserSelectedNftsApprovalStatus[index]?.approved === + SWAPLACE_SMART_CONTRACT_ADDRESS[chainId] + ) { + toast.error("Token already approved."); + } else { + await handleApprove(nft).then((result) => { + if (result != undefined) { + const nftWasApproved = (authedUserSelectedNftsApprovalStatus[ + index + ].approved = SWAPLACE_SMART_CONTRACT_ADDRESS[chainId] as any); + + setAuthedUserNftsApprovalStatus([nftWasApproved]); + } + validateApprovalTokens(authedUserSelectedNftsApprovalStatus); + }); + } + }; + + return ( +
+
+ {nftAuthUser.map((nft, index) => ( +
approveNftForSwapping(nft, index)} + > +
+ +
+
+
+ {authedUserSelectedNftsApprovalStatus[index]?.approved === + ADDRESS_ZERO ? ( +

+ {nftAuthUser[index].contractMetadata?.name} +

+ ) : ( +

+ {nftAuthUser[index].contractMetadata?.name} +

+ )} +
+
+ {authedUserSelectedNftsApprovalStatus[index]?.approved === + ADDRESS_ZERO ? ( +

+ PENDING APPROVAL +

+ ) : ( +
+

APPROVED

+
+ )} +
+
+
+ ))} +
+
+ ); +}; diff --git a/components/01-atoms/OfferExpiryConfirmSwap.tsx b/components/01-atoms/OfferExpiryConfirmSwap.tsx new file mode 100644 index 00000000..5e3bdc91 --- /dev/null +++ b/components/01-atoms/OfferExpiryConfirmSwap.tsx @@ -0,0 +1,24 @@ +interface OfferExpiryConfirmSwapProps { + expireTime: string; +} + +export const OfferExpiryConfirmSwap = ({ + expireTime +}: OfferExpiryConfirmSwapProps) => { + return ( + <> +
+

+ Offer expires in +

+

+ {expireTime} +

+
+ + ); +}; diff --git a/components/01-atoms/OfferTag.tsx b/components/01-atoms/OfferTag.tsx new file mode 100644 index 00000000..7e1f2783 --- /dev/null +++ b/components/01-atoms/OfferTag.tsx @@ -0,0 +1,32 @@ +export const OfferTag = () => { + enum TAG { + PEDING = "PENDING", + ACCEPTED = "ACCEPTED", + CANCELED = "CANCELED", + EXPIRED = "EXPIRED", + } + interface TagConfig { + body: React.ReactNode; + } + + const Tags: Record = { + [TAG.ACCEPTED]: { + body:
{TAG.ACCEPTED}
, + }, + [TAG.CANCELED]: { + body:
{TAG.CANCELED}
, + }, + [TAG.EXPIRED]: { + body:
{TAG.EXPIRED}
, + }, + [TAG.PEDING]: { + body:
{TAG.PEDING}
, + }, + }; + + return ( +
+ {Tags[TAG.PEDING].body} +
+ ); +}; diff --git a/components/01-atoms/ProgressBar.tsx b/components/01-atoms/ProgressBar.tsx new file mode 100644 index 00000000..6f9ce62f --- /dev/null +++ b/components/01-atoms/ProgressBar.tsx @@ -0,0 +1,32 @@ +import cc from "classcat"; + +interface ProgressBarProps { + currentStep: number; + numberOfItems: number; +} + +export const ProgressBar = ({ + currentStep, + numberOfItems, +}: ProgressBarProps) => { + return ( +
+ {Array(numberOfItems) + .fill(0) + .map((_, index) => { + return ( +
index, + "bg-[#353836]": currentStep <= index, + }, + ])} + /> + ); + })} +
+ ); +}; diff --git a/components/01-atoms/SearchBar.tsx b/components/01-atoms/SearchBar.tsx index 6b2b3171..78977d38 100644 --- a/components/01-atoms/SearchBar.tsx +++ b/components/01-atoms/SearchBar.tsx @@ -1,14 +1,10 @@ +/* eslint-disable import/no-named-as-default */ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable import/no-named-as-default-member */ -/* eslint-disable import/no-named-as-default */ -import { - MagnifyingGlassIcon, - SelectAuthedUserChain, - SelectDestinyChain, - SwapContext, -} from "."; +import { MagnifyingGlassIcon, SwapContext } from "@/components/01-atoms"; import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect } from "react"; +import { useTheme } from "next-themes"; import { ENS } from "web3-eth-ens"; import cc from "classcat"; import Web3 from "web3"; @@ -18,122 +14,96 @@ export const SearchBar = () => { setInputAddress, inputAddress, validateAddressToSwap, - userJustValidatedInput, + setUserJustValidatedInput, } = useContext(SwapContext); const { authenticatedUserAddress } = useAuthenticatedUser(); - const [validateAfterENSaddressLoads, setValidateAfterENSaddressLoads] = - useState(false); - const validateInput = () => { - if (authenticatedUserAddress) { - if (loadingENSaddress) { - setValidateAfterENSaddressLoads(true); - } else { - validateAddressToSwap(authenticatedUserAddress, ensNameAddress); - } - } + const { theme } = useTheme(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars + + const validateUser = (ensNameAddress: string | null) => { + if (!authenticatedUserAddress) return; + + validateAddressToSwap(authenticatedUserAddress, ensNameAddress); }; - const [ensNameAddress, setEnsNameAddress] = useState(""); - const [loadingENSaddress, setLoadingENSaddress] = useState(false); - useEffect(() => { + const getUserAddress = async () => { if (inputAddress) { if (!process.env.NEXT_PUBLIC_ALCHEMY_ETHEREUM_HTTP) { throw new Error( - "Cannot get ENS address without Alchemy Ethereum Mainnet API key" + "Cannot get ENS address without Alchemy Ethereum Mainnet API key", ); } const provider = new Web3.providers.HttpProvider( - process.env.NEXT_PUBLIC_ALCHEMY_ETHEREUM_HTTP + process.env.NEXT_PUBLIC_ALCHEMY_ETHEREUM_HTTP, ); const ens = new ENS(undefined, provider); - ens - .getOwner( - inputAddress.toLowerCase().includes(".") - ? inputAddress.toLowerCase().includes(".eth") - ? inputAddress.split(".")[1].length >= 3 - ? inputAddress - : `${inputAddress.split(".")[0]}.eth` - : inputAddress.split(".")[1].length >= 3 - ? inputAddress - : `${inputAddress.split(".")[0]}.eth` - : `${inputAddress}.eth` - ) - .then((address: unknown) => { - if (typeof address == "string") { - setEnsNameAddress(address); - setLoadingENSaddress(false); - } else { - setEnsNameAddress(""); - setLoadingENSaddress(false); - } - }) - .catch(() => { - setEnsNameAddress(""); - setLoadingENSaddress(false); - }); + const formattedAddress = inputAddress.toLowerCase().includes(".") + ? inputAddress.toLowerCase().includes(".eth") + ? inputAddress.split(".")[1].length >= 3 + ? inputAddress + : `${inputAddress.split(".")[0]}.eth` + : inputAddress.split(".")[1].length >= 3 + ? inputAddress + : `${inputAddress.split(".")[0]}.eth` + : `${inputAddress}.eth`; + + try { + const address: unknown = await ens.getOwner(formattedAddress); + + if (typeof address !== "string") return; + validateUser(address); + } catch (e) { + console.error(e); + } finally { + setUserJustValidatedInput(true); + } } - }, [inputAddress]); + }; useEffect(() => { - if (!loadingENSaddress && validateAfterENSaddressLoads) { - validateInput(); - setValidateAfterENSaddressLoads(false); - } - }, [loadingENSaddress]); + const requestDelay = setTimeout(() => { + setUserJustValidatedInput(false); + + getUserAddress(); + }, 2000); + return () => clearTimeout(requestDelay); + }, [inputAddress]); return ( -
+
-

Who are you swapping with today?

+

+ Who are you swapping with today? +

-
+
+
+ +
setInputAddress(e.target.value)} + placeholder="Search username, address or ENS" + onChange={({ target }) => setInputAddress(target.value)} /> -
-
- -
-
-
- -
-
-

Your network:

- -
-
-

Searched address network:

-
- -
-
); diff --git a/components/01-atoms/SelectAuthedUserChain.tsx b/components/01-atoms/SelectAuthedUserChain.tsx index 8a8a9250..fc36125b 100644 --- a/components/01-atoms/SelectAuthedUserChain.tsx +++ b/components/01-atoms/SelectAuthedUserChain.tsx @@ -49,7 +49,7 @@ export const SelectAuthedUserChain = () => {
+
+
+ ); +}; + +const TokenBody = () => { + enum TokenPosibilities { + ERC20 = "ERC20", + ERC721 = "ERC721", + } + + type TokenPossibilites = TokenPosibilities | "ERC20" | "ERC721"; + + const [token, setToken] = useState("ERC20"); + return ( +
+
+
What kind of token you want to add?
+
+ + +
+
+
+ {token === "ERC20" ? ( +
+
+ Contract address +
+
+ +
+
+ ) : ( +
+
+
+ Contract address +
+
+ +
+
+
+
+ Token ID +
+
+ +
+
+
+ )} +
+
+ +
+
+ ); +}; + +const AddManuallyVariantConfig: Record = + { + [AddManuallyVariant.SWAP]: { + header: "Add swap manually", + body: , + }, + [AddManuallyVariant.TOKEN]: { + header: "Add token", + body: , + }, + }; + +export const SwapAddManuallyModalLayout = ({ + variant = AddManuallyVariant.TOKEN, + open, + onClose, +}: AddManuallyProps) => { + const { theme } = useTheme(); + const { isMobile } = useScreenSize(); + return ( + +
+
+
+
+ {AddManuallyVariantConfig[variant].header} +
+
+ +
+
+
{AddManuallyVariantConfig[variant].body}
+
+
+
+ ); +}; diff --git a/components/01-atoms/SwapAddTokenCard.tsx b/components/01-atoms/SwapAddTokenCard.tsx new file mode 100644 index 00000000..3fa2faa6 --- /dev/null +++ b/components/01-atoms/SwapAddTokenCard.tsx @@ -0,0 +1,44 @@ +import { + PlusIcon, + SwapAddManuallyModalLayout, + Tooltip, +} from "@/components/01-atoms"; +import { useState } from "react"; +export const SwapAddTokenCard = () => { + const [open, setOpen] = useState(false); + + return ( + <> +
+ +
+ +
+
+ + setOpen(false)} + /> + + ); +}; diff --git a/components/01-atoms/SwapContext.tsx b/components/01-atoms/SwapContext.tsx index 919d36f6..75e12989 100644 --- a/components/01-atoms/SwapContext.tsx +++ b/components/01-atoms/SwapContext.tsx @@ -1,9 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable unused-imports/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-empty-function */ import { ADDRESS_ZERO, NFT, SupportedNetworks } from "@/lib/client/constants"; import { EthereumAddress } from "@/lib/shared/types"; +import { + ButtonClickPossibilities, + IArrayStatusTokenApproved, + SwapModalSteps, +} from "@/lib/client/blockchain-data"; import React, { Dispatch, useEffect, useState } from "react"; import toast from "react-hot-toast"; @@ -13,7 +18,7 @@ interface SwapContextProps { setInputAddress: (address: string) => void; validateAddressToSwap: ( authedUser: EthereumAddress, - inputEnsAddress: string | null | undefined + inputEnsAddress: string | null | undefined, ) => void; setUserJustValidatedInput: Dispatch>; userJustValidatedInput: boolean; @@ -23,6 +28,16 @@ interface SwapContextProps { nftInputUser: NFT[]; destinyChain: SupportedNetworks; setDestinyChain: Dispatch>; + setTimeDate: Dispatch>; + timeDate: bigint; + setAllSelectedNftsAreApproved: Dispatch>; + allSelectedNftsApproved: boolean; + setAuthedUserNftsApprovalStatus: Dispatch< + React.SetStateAction + >; + authedUserSelectedNftsApprovalStatus: IArrayStatusTokenApproved[]; + updateSwapStep: (buttonClickAction: ButtonClickPossibilities) => void; + currentSwapModalStep: SwapModalSteps; } export const SwapContext = React.createContext({ @@ -30,7 +45,7 @@ export const SwapContext = React.createContext({ validatedAddressToSwap: "", validateAddressToSwap: ( _authedUser: EthereumAddress, - _inputEnsAddress: string | null | undefined + _inputEnsAddress: string | null | undefined, ) => {}, setInputAddress: (address: string) => {}, setUserJustValidatedInput: () => {}, @@ -41,6 +56,14 @@ export const SwapContext = React.createContext({ nftInputUser: [], destinyChain: SupportedNetworks.SEPOLIA, setDestinyChain: () => {}, + setTimeDate: () => {}, + timeDate: BigInt(1), + setAllSelectedNftsAreApproved: () => {}, + allSelectedNftsApproved: false, + setAuthedUserNftsApprovalStatus: () => {}, + authedUserSelectedNftsApprovalStatus: [], + currentSwapModalStep: SwapModalSteps.APPROVE_NFTS, + updateSwapStep: (buttonClickAction: ButtonClickPossibilities) => {}, }); export const SwapContextProvider = ({ children }: any) => { @@ -50,12 +73,22 @@ export const SwapContextProvider = ({ children }: any) => { const [nftAuthUser, setNftAuthUser] = useState([]); const [nftInputUser, setNftInputUser] = useState([]); const [destinyChain, setDestinyChain] = useState( - SupportedNetworks.SEPOLIA + SupportedNetworks.SEPOLIA, ); + const [timeDate, setTimeDate] = useState(BigInt(1)); + + const [currentSwapModalStep, setCurrentSwapModalStep] = + useState(SwapModalSteps.APPROVE_NFTS); + const [allSelectedNftsApproved, setAllSelectedNftsAreApproved] = + useState(false); + const [ + authedUserSelectedNftsApprovalStatus, + setAuthedUserNftsApprovalStatus, + ] = useState([]); const validateAddressToSwap = ( _authedUser: EthereumAddress, - _inputEnsAddress: string | null | undefined + _inputEnsAddress: string | null | undefined, ) => { if (!inputAddress && !_inputEnsAddress) { toast.error("Please enter a valid address or some registered ENS domain"); @@ -96,12 +129,41 @@ export const SwapContextProvider = ({ children }: any) => { toast.success("Searching Address"); } else { toast.error( - "Your input is not a valid address and neither some registered ENS domain" + "Your input is not a valid address and neither some registered ENS domain", ); } setUserJustValidatedInput(true); }; + const updateSwapStep = (buttonClicked: ButtonClickPossibilities) => { + switch (currentSwapModalStep) { + case SwapModalSteps.APPROVE_NFTS: + if (buttonClicked === ButtonClickPossibilities.NEXT_STEP) { + setCurrentSwapModalStep(SwapModalSteps.CREATE_SWAP); + } + break; + case SwapModalSteps.CREATE_SWAP: + if (buttonClicked === ButtonClickPossibilities.PREVIOUS_STEP) { + setCurrentSwapModalStep(SwapModalSteps.APPROVE_NFTS); + } else if (buttonClicked === ButtonClickPossibilities.NEXT_STEP) { + setCurrentSwapModalStep(SwapModalSteps.CREATING_SWAP); + } + break; + case SwapModalSteps.CREATING_SWAP: + if (buttonClicked === ButtonClickPossibilities.NEXT_STEP) { + setCurrentSwapModalStep(SwapModalSteps.CREATED_SWAP); + } else if (buttonClicked === ButtonClickPossibilities.PREVIOUS_STEP) { + setCurrentSwapModalStep(SwapModalSteps.CREATE_SWAP); + } + break; + case SwapModalSteps.CREATED_SWAP: + if (buttonClicked === ButtonClickPossibilities.PREVIOUS_STEP) { + setCurrentSwapModalStep(SwapModalSteps.APPROVE_NFTS); + } + break; + } + }; + useEffect(() => { setNftInputUser([]); setUserJustValidatedInput(false); @@ -125,6 +187,14 @@ export const SwapContextProvider = ({ children }: any) => { nftInputUser, destinyChain, setDestinyChain, + setTimeDate, + timeDate, + setAllSelectedNftsAreApproved, + allSelectedNftsApproved, + setAuthedUserNftsApprovalStatus, + authedUserSelectedNftsApprovalStatus, + updateSwapStep, + currentSwapModalStep, }); }, [ inputAddress, @@ -133,6 +203,10 @@ export const SwapContextProvider = ({ children }: any) => { nftAuthUser, nftInputUser, destinyChain, + timeDate, + allSelectedNftsApproved, + authedUserSelectedNftsApprovalStatus, + currentSwapModalStep, ]); const [swapData, setSwapData] = useState({ @@ -148,6 +222,14 @@ export const SwapContextProvider = ({ children }: any) => { nftInputUser, destinyChain, setDestinyChain, + setTimeDate, + timeDate, + setAllSelectedNftsAreApproved, + allSelectedNftsApproved, + setAuthedUserNftsApprovalStatus, + authedUserSelectedNftsApprovalStatus, + updateSwapStep, + currentSwapModalStep, }); return ( diff --git a/components/01-atoms/SwapExpireTime.tsx b/components/01-atoms/SwapExpireTime.tsx new file mode 100644 index 00000000..467901b6 --- /dev/null +++ b/components/01-atoms/SwapExpireTime.tsx @@ -0,0 +1,69 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { SwapContext } from "."; +import { getTimestamp } from "@/lib/client/utils"; +import { TimeStampDate, ExpireDate } from "@/lib/client/constants"; +import React, { useContext, useEffect, useState } from "react"; +import { useNetwork } from "wagmi"; + +export const SwapExpireTime = () => { + const { chain } = useNetwork(); + const { setTimeDate } = useContext(SwapContext); + const [selectedOption, setSelectedOption] = useState( + TimeStampDate.ONE_DAY, + ); + + let chainID: number; + + if (typeof chain?.id !== "undefined") { + chainID = chain.id; + } + + const fetchData = async (selectedValue: TimeStampDate) => { + try { + const timeSelected = BigInt(selectedValue); + const timestamp = await getTimestamp(chainID); + setTimeDate(timeSelected + timestamp); + } catch (error) { + console.error("error", error); + } + }; + + const handleSelectChange = (event: React.ChangeEvent) => { + const selectedValue = event.target.value as unknown as TimeStampDate; + setSelectedOption(selectedValue); + fetchData(selectedValue); + }; + + useEffect(() => { + fetchData(selectedOption); + }, [selectedOption]); + + return ( +
+
+
+ Expires in +
+
+
+ +
+
+
+
+ ); +}; diff --git a/components/01-atoms/SwapModalButton.tsx b/components/01-atoms/SwapModalButton.tsx new file mode 100644 index 00000000..44f47b29 --- /dev/null +++ b/components/01-atoms/SwapModalButton.tsx @@ -0,0 +1,123 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { LoadingIndicator } from "./LoadingIndicator"; +import { LeftIcon, RightIcon, SwapContext } from "@/components/01-atoms"; +import React, { ButtonHTMLAttributes, useContext } from "react"; +import { useTheme } from "next-themes"; +import cc from "classcat"; + +export enum ButtonVariant { + DEFAULT, + ALTERNATIVE, + SECONDARY, +} + +type ArrowColorCondition = ( + theme: string, + tokenApproved: boolean, +) => ArrowColor; + +export enum ArrowColor { + BLACK = "#000000", + GRAY = "#707572", + WHITE = "#FFFFFF", + YELLOW = "#DDF23D", +} + +interface ButtonVariantConfig { + arrowColorInHex: ArrowColorCondition; + isLoading?: boolean; + style: string; +} + +const ButtonVariantsConfigs: Record = { + [ButtonVariant.DEFAULT]: { + style: + "border border-[#353836] bg-[#282B29] rounded-[10px] px-4 py-2 p-medium dark:p-medium-2-small h-9 flex justify-center items-center gap-3", + arrowColorInHex: (theme, tokenApproved) => + theme === "dark" && tokenApproved === true + ? ArrowColor.BLACK + : ArrowColor.GRAY, + }, + [ButtonVariant.ALTERNATIVE]: { + style: + "border border-[#353836] bg-[#DDF23D] bg-opacity-20 rounded-[10px] px-4 py-2 dark:p-medium p-medium-dark h-9 flex justify-center items-center gap-2 dark:!text-[#DDF23D] !text-black", + arrowColorInHex: (theme, tokenApproved) => + theme === "dark" && tokenApproved === true + ? ArrowColor.YELLOW + : ArrowColor.BLACK, + }, + + [ButtonVariant.SECONDARY]: { + style: + "border border-[#353836] bg-[#282B29] rounded-[10px] px-4 py-2 dark:p-medium-bold !text-[#181A19] p-medium-bold-dark disabled:pointer-events-none shadow justify-center items-center gap-3", + arrowColorInHex: (theme, tokenApproved) => + theme === "dark" && tokenApproved === true + ? ArrowColor.BLACK + : ArrowColor.GRAY, + }, +}; + +interface Props extends ButtonHTMLAttributes { + variant?: ButtonVariant; + label: string; + onClick?: () => void; + aditionalStyle?: string; + isLoading?: boolean; +} + +export function SwapModalButton({ + variant = ButtonVariant.DEFAULT, + label, + onClick = () => {}, + aditionalStyle, + isLoading = false, + ...props +}: Props) { + const { allSelectedNftsApproved } = useContext(SwapContext); + const { theme } = useTheme(); + if (theme === undefined) return false; + + return ( + + ); +} diff --git a/components/01-atoms/SwapModalLayout.tsx b/components/01-atoms/SwapModalLayout.tsx new file mode 100644 index 00000000..9d31440c --- /dev/null +++ b/components/01-atoms/SwapModalLayout.tsx @@ -0,0 +1,107 @@ +import { CloseIcon } from "./icons/CloseIcon"; +import { Transition, Dialog } from "@headlessui/react"; +import React, { Fragment } from "react"; +import cc from "classcat"; +import { useTheme } from "next-themes"; + +interface ConfirmSwapModalToggle { + open: boolean; + onClose: () => void; +} + +interface ConfirmSwapModalText { + title: string; + description?: string; +} + +interface ConfirmSwapApprovalModalBody { + component: React.ReactNode; +} + +interface ConfirmSwapApprovalModalFooter { + component: React.ReactNode; +} + +interface ISwapModalLayout { + toggleCloseButton: ConfirmSwapModalToggle; + text: ConfirmSwapModalText; + body: ConfirmSwapApprovalModalBody; + footer: ConfirmSwapApprovalModalFooter; +} + +export const SwapModalLayout = ({ + toggleCloseButton, + text, + body, + footer, +}: ISwapModalLayout) => { + const { theme } = useTheme(); + + return ( + <> + +
+ + + + +
+ +

+ {text.title} +

+
+ +
+
+
+ +
+
+ +

+ {text.description} +

+
+
+ +
{body.component}
+
+ +
+ {footer.component} +
+
+
+
+ + ); +}; diff --git a/components/01-atoms/SwappingIcons.tsx b/components/01-atoms/SwappingIcons.tsx new file mode 100644 index 00000000..cac7d3ce --- /dev/null +++ b/components/01-atoms/SwappingIcons.tsx @@ -0,0 +1,155 @@ +import { + SwappingIcon, + OffersIcon, + ChatIcon, + NotificationsIcon, + Tooltip, +} from "@/components/01-atoms"; + +import { useScreenSize } from "@/lib/client/hooks/useScreenSize"; +import { useTheme } from "next-themes"; +import { NextRouter, useRouter } from "next/router"; +import { SVGProps, useState } from "react"; +import cc from "classcat"; + +export interface IconSwap { + id: number; + name: string; + href: string; + icon: (props: SVGProps) => JSX.Element; + disabled?: boolean; +} + +export enum SwappingIconsID { + "SWAPLACE_STATION", + "OFFERS", + "CHAT", + "NOTIFICATIONS", +} + +const findInitialActiveTab = ( + swappingTabs: Array, + router: NextRouter, +) => { + const matchingTab = swappingTabs.find((tab) => router.pathname === tab.href); + return matchingTab ? matchingTab.id : SwappingIconsID.SWAPLACE_STATION; +}; + +export const SwappingIcons = () => { + const { theme } = useTheme(); + const { isWideScreen } = useScreenSize(); + + const swappingTabs: Array = [ + { + id: SwappingIconsID.SWAPLACE_STATION, + name: "Swaplace Station", + href: "/", + icon: SwappingIcon, + }, + { + id: SwappingIconsID.OFFERS, + name: "Offers", + href: "/offers", + icon: OffersIcon, + }, + { + id: SwappingIconsID.CHAT, + name: "Chat", + href: "/", + icon: ChatIcon, + disabled: true, + }, + { + id: SwappingIconsID.NOTIFICATIONS, + name: "Notifications", + href: "/", + icon: NotificationsIcon, + disabled: true, + }, + ]; + + const router = useRouter(); + + const [activeTab, setActiveTab] = useState( + findInitialActiveTab(swappingTabs, router), + ); + + const handleClick = async (e: IconSwap) => { + setActiveTab(e.id); + router.push(e.href); + }; + + return ( + <> + {swappingTabs.map((swappingTab) => { + const IconComponent = swappingTab.icon; + const isSelected = activeTab == swappingTab.id; + const isDisabled = swappingTab.disabled; + + return ( +
+ {isWideScreen ? ( + +
{ + !isDisabled && handleClick(swappingTab); + }} + > +
+ +
+
+
+ ) : ( + +
{ + handleClick(swappingTab); + }} + > +
+ +
+
+
+ )} +
+ ); + })} + + ); +}; diff --git a/components/01-atoms/Tab.tsx b/components/01-atoms/Tab.tsx index 499431c8..11fe780e 100644 --- a/components/01-atoms/Tab.tsx +++ b/components/01-atoms/Tab.tsx @@ -30,14 +30,16 @@ export const Tab = ({ setActiveSwappingShelfID }: ITab) => { const [isActiveTab, setIsActiveTab] = useState(SwappingShelfID.THEIR_ITEMS); return ( -
+
{swappingTabs.map((tab) => { return (
{ diff --git a/components/01-atoms/ThreeDotsCardOffersOptions.tsx b/components/01-atoms/ThreeDotsCardOffersOptions.tsx new file mode 100644 index 00000000..3e62d9bf --- /dev/null +++ b/components/01-atoms/ThreeDotsCardOffersOptions.tsx @@ -0,0 +1,56 @@ +import { ShareIcon, ThreeDotsIcon, XMarkIcon } from "@/components/01-atoms"; +import { useState } from "react"; +import cc from "classcat"; + +export const ThreeDotsCardOffersOptions = () => { + const [isOpen, setIsOpen] = useState(false); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + return ( +
+
+
+ +
+ + {isOpen && ( +
+
+ + +
+
+ )} +
+
+ ); +}; diff --git a/components/01-atoms/TokenCardProperties.tsx b/components/01-atoms/TokenCardProperties.tsx new file mode 100644 index 00000000..f0ab9ce8 --- /dev/null +++ b/components/01-atoms/TokenCardProperties.tsx @@ -0,0 +1,28 @@ +interface PropertiesInfo { + amount: number; + value: number; +} + +interface TokenCardPropertiesProps { + properties: PropertiesInfo; +} + +export const TokenCardProperties = ({ + properties, +}: TokenCardPropertiesProps) => { + return ( +
+
+
{properties.amount} item(s)
+
+

+ {properties.value} ETH +

+

+   (${properties.value}) +

+
+
+
+ ); +}; diff --git a/components/01-atoms/TokenOfferDetails.tsx b/components/01-atoms/TokenOfferDetails.tsx new file mode 100644 index 00000000..ffa4fd95 --- /dev/null +++ b/components/01-atoms/TokenOfferDetails.tsx @@ -0,0 +1,37 @@ +import { + DoneIcon, + OfferTag, + ThreeDotsCardOffersOptions, +} from "@/components/01-atoms"; +import React from "react"; + +export const TokenOfferDetails = () => ( +
+
+
    + +
  • +
    + Expires on 16 Oct, 2023 +
  • +
  • +
    + Created by you +
  • +
+
+
+
+ +
+ + +
+
+); diff --git a/components/01-atoms/TokensOfferSkeleton.tsx b/components/01-atoms/TokensOfferSkeleton.tsx new file mode 100644 index 00000000..ff526c21 --- /dev/null +++ b/components/01-atoms/TokensOfferSkeleton.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import cc from "classcat"; + +export const TokensOfferSkeleton = () => { + const TokenCardSkeleton = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + }; + + const TokenOfferDetailsSkeleton = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); + }; + + return ( +
+
+
+ +
+
+ +
+
+ +
+ ); +}; diff --git a/components/01-atoms/Tooltip.tsx b/components/01-atoms/Tooltip.tsx new file mode 100644 index 00000000..97e0fe3e --- /dev/null +++ b/components/01-atoms/Tooltip.tsx @@ -0,0 +1,80 @@ +import PropTypes from "prop-types"; +import cc from "classcat"; + +enum ToolTipPosition { + TOP = "top", + BOTTOM = "bottom", + LEFT = "left", + RIGHT = "right", +} + +type Position = ToolTipPosition | "top" | "bottom" | "left" | "right"; + +interface TooltipProps { + position: Position; + content: string; + children?: React.ReactNode; +} + +export const Tooltip = ({ position, content, children }: TooltipProps) => ( +
+
{children}
+
+ + +
+
+); + +Tooltip.propTypes = { + position: PropTypes.oneOf([ + ToolTipPosition.TOP, + ToolTipPosition.BOTTOM, + ToolTipPosition.LEFT, + ToolTipPosition.RIGHT, + ]).isRequired, + content: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, +}; diff --git a/components/01-atoms/TransactionResultModal.tsx b/components/01-atoms/TransactionResultModal.tsx index 65401fe9..d862d42d 100644 --- a/components/01-atoms/TransactionResultModal.tsx +++ b/components/01-atoms/TransactionResultModal.tsx @@ -1,81 +1,80 @@ -import { TransactionResult } from "."; -import LoadingIndicator from "./LoadingIndicator"; -import { DangerIcon } from "./icons/DangerIcon"; -import { Dialog, Transition } from "@headlessui/react"; -import { CheckmarkIcon } from "react-hot-toast"; -import { Fragment, useState } from "react"; +// import { Fragment, useState } from "react"; +// import { Dialog, Transition } from "@headlessui/react"; +// import { TransactionResult, LoadingIndicator } from "@/components/01-atoms"; +// import { DangerIcon } from "@/components/01-atoms/icons"; +// import { CheckmarkIcon } from "react-hot-toast"; -interface TransactionResultModalProps { - onClose: () => void; - transactionResult: TransactionResult | null; -} +// interface TransactionResultModalProps { +// onClose: () => void; +// transactionResult: TransactionResult | null; +// } -export const TransactionResultModal = ({ - transactionResult, - onClose, -}: TransactionResultModalProps) => { - const [open, setOpen] = useState(true); +// export const TransactionResultModal = ({ +// transactionResult, +// onClose, +// }: TransactionResultModalProps) => { +// if (!transactionResult) { +// return null; +// } - if (!transactionResult) { - return null; - } +// const [open, setOpen] = useState(true); - const closeModal = () => { - setOpen(false); - onClose(); - }; +// const closeModal = () => { +// setOpen(false); +// onClose(); +// }; - return ( - <> - -
- - - - - {transactionResult === TransactionResult.LOADING ? ( -
- -

- Loading -

-
- ) : transactionResult === TransactionResult.FAILURE ? ( -
- -

- Could not create offer -

-
- ) : transactionResult === TransactionResult.SUCCESS ? ( -
- -

- Offer created successfully -

-
- ) : null} -
-
-
- - ); -}; +// return ( +// <> +// +//
+// +// +// +// +// {transactionResult === TransactionResult.LOADING ? ( +//
+// +//

+// Loading +//

+//
+// ) : transactionResult === TransactionResult.FAILURE ? ( +//
+// +//

+// Could not create offer +//

+//
+// ) : transactionResult === TransactionResult.SUCCESS ? ( +//
+// +//

+// Offer created successfully +//

+//
+// ) : null} +//
+//
+//
+// +// ); +// }; diff --git a/components/01-atoms/WalletSidebarTemplate.tsx b/components/01-atoms/WalletSidebarTemplate.tsx new file mode 100644 index 00000000..02e0655e --- /dev/null +++ b/components/01-atoms/WalletSidebarTemplate.tsx @@ -0,0 +1,45 @@ +import { TheSidebarHeader, UserInfo } from "@/components/02-molecules"; +import React from "react"; +import cc from "classcat"; +import { useTheme } from "next-themes"; + +interface WalletSidebarTemplateProps { + isOpen: boolean; + isMobile: boolean; +} + +export const WalletSidebarTemplate = ({ + isOpen, + isMobile, +}: WalletSidebarTemplateProps) => { + const { systemTheme, theme } = useTheme(); + const currentTheme = theme === "system" ? systemTheme : theme; + const isDark = currentTheme === "dark"; + + return ( + <> +
+ +
+
+ + +
+
+ + ); +}; diff --git a/components/01-atoms/icons/ChatIcon.tsx b/components/01-atoms/icons/ChatIcon.tsx new file mode 100644 index 00000000..29a89118 --- /dev/null +++ b/components/01-atoms/icons/ChatIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const ChatIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/CloseIcon.tsx b/components/01-atoms/icons/CloseIcon.tsx new file mode 100644 index 00000000..c7e9b65d --- /dev/null +++ b/components/01-atoms/icons/CloseIcon.tsx @@ -0,0 +1,24 @@ +import { SVGProps } from "react"; + +export const CloseIcon = (props: SVGProps) => { + return ( +
+ + + + + +
+ ); +}; diff --git a/components/01-atoms/icons/CopyIcon.tsx b/components/01-atoms/icons/CopyIcon.tsx new file mode 100644 index 00000000..bcd3f787 --- /dev/null +++ b/components/01-atoms/icons/CopyIcon.tsx @@ -0,0 +1,19 @@ +import React, { SVGProps } from "react"; + +export const CopyIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/icons/DoneIcon.tsx b/components/01-atoms/icons/DoneIcon.tsx new file mode 100644 index 00000000..82550e34 --- /dev/null +++ b/components/01-atoms/icons/DoneIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const DoneIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/ErrorIcon.tsx b/components/01-atoms/icons/ErrorIcon.tsx new file mode 100644 index 00000000..ec9dd9a2 --- /dev/null +++ b/components/01-atoms/icons/ErrorIcon.tsx @@ -0,0 +1,52 @@ +import { SVGProps } from "react"; +import cc from "classcat"; +import { useTheme } from "next-themes"; + +export const ErrorIcon = (props: SVGProps) => { + const { theme } = useTheme(); + return ( +
+ + + + + + + + + + + +
+ ); +}; diff --git a/components/01-atoms/icons/ExternalLinkIcon.tsx b/components/01-atoms/icons/ExternalLinkIcon.tsx new file mode 100644 index 00000000..537433f7 --- /dev/null +++ b/components/01-atoms/icons/ExternalLinkIcon.tsx @@ -0,0 +1,19 @@ +import React, { SVGProps } from "react"; + +export const ExternalLinkIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/icons/LeftIcon.tsx b/components/01-atoms/icons/LeftIcon.tsx new file mode 100644 index 00000000..c3858400 --- /dev/null +++ b/components/01-atoms/icons/LeftIcon.tsx @@ -0,0 +1,22 @@ +import React, { SVGProps } from "react"; + +export const LeftIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/MoonIcon.tsx b/components/01-atoms/icons/MoonIcon.tsx new file mode 100644 index 00000000..eb96d48d --- /dev/null +++ b/components/01-atoms/icons/MoonIcon.tsx @@ -0,0 +1,9 @@ +import { SVGProps } from "react"; + +export const MoonIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/icons/NoSwapsIcon.tsx b/components/01-atoms/icons/NoSwapsIcon.tsx new file mode 100644 index 00000000..9ff5e0f4 --- /dev/null +++ b/components/01-atoms/icons/NoSwapsIcon.tsx @@ -0,0 +1,88 @@ +import { SVGProps } from "react"; + +export const NoSwapsIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/components/01-atoms/icons/NotificationsIcon.tsx b/components/01-atoms/icons/NotificationsIcon.tsx new file mode 100644 index 00000000..6929d741 --- /dev/null +++ b/components/01-atoms/icons/NotificationsIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const NotificationsIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/OffersIcon.tsx b/components/01-atoms/icons/OffersIcon.tsx new file mode 100644 index 00000000..0993383f --- /dev/null +++ b/components/01-atoms/icons/OffersIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const OffersIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/PersonIcon.tsx b/components/01-atoms/icons/PersonIcon.tsx new file mode 100644 index 00000000..2b830f3b --- /dev/null +++ b/components/01-atoms/icons/PersonIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const PersonIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/PlusIcon.tsx b/components/01-atoms/icons/PlusIcon.tsx new file mode 100644 index 00000000..52549762 --- /dev/null +++ b/components/01-atoms/icons/PlusIcon.tsx @@ -0,0 +1,19 @@ +export const PlusIcon = () => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/PowerIcon.tsx b/components/01-atoms/icons/PowerIcon.tsx new file mode 100644 index 00000000..f76359b4 --- /dev/null +++ b/components/01-atoms/icons/PowerIcon.tsx @@ -0,0 +1,26 @@ +import { SVGProps } from "react"; + +export const PowerIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/components/01-atoms/icons/RightIcon.tsx b/components/01-atoms/icons/RightIcon.tsx new file mode 100644 index 00000000..e86f7454 --- /dev/null +++ b/components/01-atoms/icons/RightIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const RightIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/SelectUserIcon.tsx b/components/01-atoms/icons/SelectUserIcon.tsx new file mode 100644 index 00000000..73677def --- /dev/null +++ b/components/01-atoms/icons/SelectUserIcon.tsx @@ -0,0 +1,23 @@ +import { SVGProps } from "react"; + +export const SelectUserIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/icons/ShareIcon.tsx b/components/01-atoms/icons/ShareIcon.tsx new file mode 100644 index 00000000..2a3dcc73 --- /dev/null +++ b/components/01-atoms/icons/ShareIcon.tsx @@ -0,0 +1,22 @@ +import React, { SVGProps } from "react"; + +export const ShareIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/SunIcon.tsx b/components/01-atoms/icons/SunIcon.tsx new file mode 100644 index 00000000..ecbe98d2 --- /dev/null +++ b/components/01-atoms/icons/SunIcon.tsx @@ -0,0 +1,17 @@ +import { SVGProps } from "react"; + +export const SunIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/components/01-atoms/icons/SwapIcon.tsx b/components/01-atoms/icons/SwapIcon.tsx index 48b27697..918142fa 100644 --- a/components/01-atoms/icons/SwapIcon.tsx +++ b/components/01-atoms/icons/SwapIcon.tsx @@ -3,30 +3,31 @@ import { SVGProps } from "react"; export const SwapIcon = (props: SVGProps) => { return ( - + - + ) => { values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> - + - - - ); diff --git a/components/01-atoms/icons/SwaplaceIcon.tsx b/components/01-atoms/icons/SwaplaceIcon.tsx index d4a68ad7..d5561011 100644 --- a/components/01-atoms/icons/SwaplaceIcon.tsx +++ b/components/01-atoms/icons/SwaplaceIcon.tsx @@ -4,7 +4,7 @@ export const SwaplaceIcon = (props: SVGProps) => { return ( @@ -12,12 +12,12 @@ export const SwaplaceIcon = (props: SVGProps) => { diff --git a/components/01-atoms/icons/SwappingIcon.tsx b/components/01-atoms/icons/SwappingIcon.tsx new file mode 100644 index 00000000..6bee19e0 --- /dev/null +++ b/components/01-atoms/icons/SwappingIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const SwappingIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/ThreeDotsIcon.tsx b/components/01-atoms/icons/ThreeDotsIcon.tsx new file mode 100644 index 00000000..8e4f42de --- /dev/null +++ b/components/01-atoms/icons/ThreeDotsIcon.tsx @@ -0,0 +1,20 @@ +import { SVGProps } from "react"; + +export const ThreeDotsIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/icons/WalletIcon.tsx b/components/01-atoms/icons/WalletIcon.tsx new file mode 100644 index 00000000..b3d6ebd6 --- /dev/null +++ b/components/01-atoms/icons/WalletIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const WalletIcon = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/components/01-atoms/icons/XMarkIcon.tsx b/components/01-atoms/icons/XMarkIcon.tsx new file mode 100644 index 00000000..559f8f22 --- /dev/null +++ b/components/01-atoms/icons/XMarkIcon.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from "react"; + +export const XMarkIcon = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/components/01-atoms/index.ts b/components/01-atoms/index.ts index 988081da..2cc92446 100644 --- a/components/01-atoms/index.ts +++ b/components/01-atoms/index.ts @@ -1,15 +1,53 @@ +export * from "./SwapModalLayout"; export * from "./ConnectWallet"; -export * from "../02-molecules/NftCard"; +export * from "./EmptyNftsCards"; +export * from "./LoadingIndicator"; +export * from "./OfferExpiryConfirmSwap"; +export * from "./OfferTag"; +export * from "./NftsCardApprovedList"; +export * from "./ProgressBar"; export * from "./SearchBar"; export * from "./SelectAuthedUserChain"; -export * from "./Tab"; -export * from "./SwapContext"; -export * from "./icons/SwaplaceIcon"; -export * from "./icons/MagnifyingGlassIcon"; -export * from "./icons/SwapIcon"; -export * from "./icons/PaperPlane"; export * from "./SelectDestinyChain"; +export * from "./StatusOffers"; +export * from "./SwapAddTokenCard"; +export * from "./SwapAddManuallyModalLayout"; +export * from "./SwapContext"; +export * from "./SwapExpireTime"; +export * from "./SwapModalButton"; +export * from "./TokenOfferDetails"; +export * from "./SwappingIcons"; +export * from "./Tab"; +export * from "./TokenCardProperties"; +export * from "./Tooltip"; +export * from "./TokensOfferSkeleton"; +export * from "./ThreeDotsCardOffersOptions"; +export * from "./WalletSidebarTemplate"; +export * from "./ENSAvatar"; +export * from "./icons/ChatIcon"; +export * from "./icons/DoneIcon"; +export * from "./icons/DangerIcon"; export * from "./icons/EthereumIcon"; +export * from "./icons/ErrorIcon"; +export * from "./icons/LeftIcon"; +export * from "./icons/NotificationsIcon"; +export * from "./icons/OffersIcon"; +export * from "./icons/PersonIcon"; +export * from "./icons/PlusIcon"; export * from "./icons/PolygonIcon"; -export * from "./ConfirmSwapModal"; -export * from "./TransactionResultModal"; +export * from "./icons/RightIcon"; +export * from "./icons/SelectUserIcon"; +export * from "./icons/SwapIcon"; +export * from "./icons/SwaplaceIcon"; +export * from "./icons/SwappingIcon"; +export * from "./icons/XMarkIcon"; +export * from "./icons/WalletIcon"; +export * from "./icons/PaperPlane"; +export * from "./icons/ExternalLinkIcon"; +export * from "./icons/SunIcon"; +export * from "./icons/CopyIcon"; +export * from "./icons/MagnifyingGlassIcon"; +export * from "./icons/MoonIcon"; +export * from "./icons/ThreeDotsIcon"; +export * from "./icons/NoSwapsIcon"; +export * from "./icons/ShareIcon"; diff --git a/components/02-molecules/AccountBalanceWalletSidebar.tsx b/components/02-molecules/AccountBalanceWalletSidebar.tsx new file mode 100644 index 00000000..d986ad03 --- /dev/null +++ b/components/02-molecules/AccountBalanceWalletSidebar.tsx @@ -0,0 +1,33 @@ +import { useWalletBalance } from "@/lib/client/hooks/useWalletBalance"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import React from "react"; +import cc from "classcat"; +import { useTheme } from "next-themes"; +import { useNetwork } from "wagmi"; + +export const AccountBalanceWalletSidebar = () => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + + const { chain } = useNetwork(); + + const { systemTheme, theme } = useTheme(); + const currentTheme = theme === "system" ? systemTheme : theme; + const isDark = currentTheme === "dark"; + + const { balance } = useWalletBalance({ + walletAddress: authenticatedUserAddress, + }); + + const match = balance?.match(/^(\d+\.\d{1,3})|\d+/); + const displayBalance = match ? match[0] : balance; + + return ( +
+

Current balance

+
+

{displayBalance}

+

{chain?.nativeCurrency.symbol}

+
+
+ ); +}; diff --git a/components/02-molecules/CardHome.tsx b/components/02-molecules/CardHome.tsx index f36f591b..5f9f60e3 100644 --- a/components/02-molecules/CardHome.tsx +++ b/components/02-molecules/CardHome.tsx @@ -7,14 +7,14 @@ export const CardHome = () => {
-

Swaplace

+

Swaplace

-
+
Your greatest deals are here
-
+
Connect your wallet to start swapping your NFTs and tokens
@@ -22,7 +22,7 @@ export const CardHome = () => {
diff --git a/components/02-molecules/CardOffers.tsx b/components/02-molecules/CardOffers.tsx new file mode 100644 index 00000000..02e2d7e8 --- /dev/null +++ b/components/02-molecules/CardOffers.tsx @@ -0,0 +1,42 @@ +import { NftCard, UserOfferInfo } from "@/components/02-molecules"; +import { SwapContext, TokenCardProperties } from "@/components/01-atoms"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { EthereumAddress } from "@/lib/shared/types"; +import { useContext } from "react"; + +interface CardOffersProps { + address: EthereumAddress | null; +} + +export const CardOffers = ({ address }: CardOffersProps) => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + const { nftAuthUser } = useContext(SwapContext); + + return ( +
+
+
+ +
+
+ {authenticatedUserAddress && ( // That div needs change to render the given Tokens by Subgraph, shouldn't be the , for now, just visualization +
+ {nftAuthUser.map((nft, index) => ( + + ))} +
+ )} +
+
+ +
+
+
+ ); +}; diff --git a/components/02-molecules/ConfirmSwapModal.tsx b/components/02-molecules/ConfirmSwapModal.tsx new file mode 100644 index 00000000..461df4a0 --- /dev/null +++ b/components/02-molecules/ConfirmSwapModal.tsx @@ -0,0 +1,259 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { + ButtonVariant, + NftsCardApprovedList, + SwapContext, + SwapModalButton, + SwapModalLayout, + OfferExpiryConfirmSwap +} from "@/components/01-atoms"; +import { ProgressStatus } from "@/components/02-molecules"; +import { createSwap } from "@/lib/service/createSwap"; +import { + ButtonClickPossibilities, + ComposeNftSwap, + ICreateSwap, + SwapModalSteps, +} from "@/lib/client/blockchain-data"; +import { updateNftsToSwapApprovalStatus } from "@/lib/client/swap-utils"; +import { useNetwork, useWalletClient } from "wagmi"; +import { useContext, useEffect } from "react"; +import { useTheme } from "next-themes"; +import toast from "react-hot-toast"; + +interface ConfirmSwapApprovalModalProps { + open: boolean; + onClose: () => void; +} + +export const ConfirmSwapModal = ({ + open, + onClose, +}: ConfirmSwapApprovalModalProps) => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + const { + timeDate, + nftAuthUser, + nftInputUser, + allSelectedNftsApproved, + validatedAddressToSwap, + setAuthedUserNftsApprovalStatus, + setAllSelectedNftsAreApproved, + currentSwapModalStep, + updateSwapStep, + } = useContext(SwapContext); + + const { chain } = useNetwork(); + const { data: walletClient } = useWalletClient(); + const { theme } = useTheme(); + + useEffect(() => { + if (currentSwapModalStep === SwapModalSteps.CREATING_SWAP) { + handleSwap(); + } + }, [currentSwapModalStep]); + + useEffect(() => { + if (!open) { + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + } + }, [open]); + + const nftsInputUser = ComposeNftSwap(nftInputUser); + const nftsAuthUser = ComposeNftSwap(nftAuthUser); + + useEffect(() => { + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + + const fetchApprove = async () => { + await updateNftsToSwapApprovalStatus( + nftAuthUser, + setAuthedUserNftsApprovalStatus, + setAllSelectedNftsAreApproved, + ); + }; + fetchApprove(); + }, [nftAuthUser, allSelectedNftsApproved]); + + if (!authenticatedUserAddress?.address || !nftInputUser || !nftAuthUser) { + onClose(); + return null; + } + + let chainId: number; + + const handleSwap = async () => { + if (typeof chain?.id != "undefined") { + chainId = chain?.id; + } else { + throw new Error("Chain ID is undefined"); + } + + const swapData: ICreateSwap = { + walletClient: walletClient, + expireDate: timeDate, + nftInputUser: nftsInputUser, + nftAuthUser: nftsAuthUser, + validatedAddressToSwap: validatedAddressToSwap, + authenticatedUserAddress: authenticatedUserAddress, + chain: chainId, + }; + + try { + if (allSelectedNftsApproved) { + const transactionReceipt = await createSwap(swapData); + + if (transactionReceipt != undefined) { + toast.success("Created Swap"); + updateSwapStep(ButtonClickPossibilities.NEXT_STEP); + } else { + toast.error("Create Swap Failed"); + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + } + } else { + toast.error("You must approve the Tokens to create Swap."); + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + } + } catch (error) { + toast.error("You must approve the Tokens to create Swap."); + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + console.error(error); + } + }; + + const validateTokensAreApproved = () => { + if (allSelectedNftsApproved) { + if (currentSwapModalStep === SwapModalSteps.APPROVE_NFTS) { + updateSwapStep(ButtonClickPossibilities.NEXT_STEP); + } + } else { + toast.error("You must approve the Tokens to create Swap."); + } + }; + + const ConfirmSwapModalStep: Partial> = { + [SwapModalSteps.APPROVE_NFTS]: ( + , + }} + footer={{ + component: ( + <> +
+
+ +
+
+ +
+
+ + ), + }} + /> + ), + [SwapModalSteps.CREATE_SWAP]: ( + , + }} + footer={{ + component: ( + <> +
+ { + updateSwapStep(ButtonClickPossibilities.PREVIOUS_STEP); + }} + /> + + { + updateSwapStep(ButtonClickPossibilities.NEXT_STEP); + }} + /> +
+ + ), + }} + /> + ), + [SwapModalSteps.CREATING_SWAP]: ( + , + }} + footer={{ + component: ( + <> +
+ +
+ + ), + }} + /> + ), + [SwapModalSteps.CREATED_SWAP]: ( + , + }} + footer={{ + component: ( + <> +
+ +
+ + ), + }} + /> + ), + }; + + return ConfirmSwapModalStep[currentSwapModalStep]; +}; diff --git a/components/02-molecules/EnsNameAndAddressWallet.tsx b/components/02-molecules/EnsNameAndAddressWallet.tsx new file mode 100644 index 00000000..fa69c840 --- /dev/null +++ b/components/02-molecules/EnsNameAndAddressWallet.tsx @@ -0,0 +1,70 @@ +import { CopyIcon, ENSAvatar, ExternalLinkIcon } from "@/components/01-atoms"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { useEnsData } from "@/lib/client/hooks/useENSData"; +import { collapseAddress } from "@/lib/client/utils"; +import React from "react"; +import { useNetwork } from "wagmi"; + +export const EnsNameAndAddressWallet = () => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + const stringAddress = authenticatedUserAddress?.toString(); + + const { primaryName } = useEnsData({ + ensAddress: authenticatedUserAddress, + }); + + const { chain } = useNetwork(); + + const blockExplorer = `${chain?.blockExplorers?.default.url}/address/${stringAddress}`; + + const displayAddress = collapseAddress(stringAddress ?? "") || ""; + + return ( +
+ {authenticatedUserAddress && ( + <> + +
+
+ {primaryName && ( + <> +

{`${primaryName}`}

+

+ | +

+ + )} + +
+ +

+ View on explorer +

+
+ +
+
+
+ + )} +
+ ); +}; diff --git a/components/02-molecules/FilterOffers.tsx b/components/02-molecules/FilterOffers.tsx new file mode 100644 index 00000000..c69626d1 --- /dev/null +++ b/components/02-molecules/FilterOffers.tsx @@ -0,0 +1,16 @@ +import { StatusOffers } from "@/components/01-atoms"; + +export const FilterOffers = () => { + return ( +
+
+
+

Your offers

+
+
+ +
+
+
+ ); +}; diff --git a/components/02-molecules/NftCard.tsx b/components/02-molecules/NftCard.tsx index cf2b4f33..535c5052 100644 --- a/components/02-molecules/NftCard.tsx +++ b/components/02-molecules/NftCard.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { SwapContext } from "../01-atoms"; import { NFT } from "@/lib/client/constants"; +import { SwapContext } from "@/components/01-atoms"; import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; import { EthereumAddress } from "@/lib/shared/types"; import React, { useContext, useEffect, useState } from "react"; @@ -12,6 +12,7 @@ interface INftCard { ownerAddress: string | null; onClickAction?: NftCardActionType; withSelectionValidation?: boolean; + styleType?: StyleVariant; } /** @@ -26,43 +27,63 @@ interface INftCard { export enum NftCardActionType { "SELECT_NFT_FOR_SWAP", "SHOW_NFT_DETAILS", + "NFT_ONCLICK", } +export enum NftCardStyleType { + SMALL = "small", + NORMAL = "normal", + MEDIUM = "medium", + LARGE = "large", +} + +type StyleVariant = NftCardStyleType | "small" | "normal" | "medium" | "large"; + +const NftSizeClassNames = { + [NftCardStyleType.SMALL]: "card-nft-small", + [NftCardStyleType.NORMAL]: "card-nft-normal", + [NftCardStyleType.MEDIUM]: "card-nft-medium", + [NftCardStyleType.LARGE]: "card-nft-large", +}; + export const NftCard = ({ nftData, ownerAddress, withSelectionValidation = true, + styleType = NftCardStyleType.NORMAL, onClickAction = NftCardActionType.SELECT_NFT_FOR_SWAP, }: INftCard) => { const { authenticatedUserAddress } = useAuthenticatedUser(); const { setNftAuthUser, setNftInputUser, nftAuthUser, nftInputUser } = useContext(SwapContext); - const [couldntLoadNftImage, setCouldntLoadNftImage] = useState(false); const [currentNftIsSelected, setCurrentNftIsSelected] = useState(false); - - useEffect(() => { - setCouldntLoadNftImage(false); - }, [nftData]); + const [couldntLoadNftImage, setCouldntLoadNftImage] = useState(false); useEffect(() => { const currentNftIsFromAuthedUser = ownerAddress ? authenticatedUserAddress?.equals(new EthereumAddress(ownerAddress)) - : null; + : false; if (currentNftIsFromAuthedUser) { - if (nftAuthUser.length) { - setCurrentNftIsSelected(nftAuthUser[0].id === nftData.id); - } else { - setCurrentNftIsSelected(false); - } + setCurrentNftIsSelected( + nftAuthUser.some((selectedNft) => selectedNft.id === nftData.id), + ); } else { - if (nftInputUser.length) { - setCurrentNftIsSelected(nftInputUser[0].id === nftData.id); - } else { - setCurrentNftIsSelected(false); - } + setCurrentNftIsSelected( + nftInputUser.some((selectedNft) => selectedNft.id === nftData.id), + ); } - }, [authenticatedUserAddress, ownerAddress, nftAuthUser, nftInputUser]); + }, [ + authenticatedUserAddress, + ownerAddress, + nftAuthUser, + nftInputUser, + nftData, + ]); + + useEffect(() => { + setCouldntLoadNftImage(false); + }, [nftData]); if (!nftData || !nftData.id || !nftData.contract || !ownerAddress) return null; @@ -72,23 +93,38 @@ export const NftCard = ({ const ownerEthAddress = new EthereumAddress(ownerAddress); if (authenticatedUserAddress?.equals(ownerEthAddress)) { - if (nftAuthUser.length) { - if (nftAuthUser[0].id === nftData.id) setNftAuthUser([]); - else setNftAuthUser([nftData]); + const isSelected = nftAuthUser.some( + (selectedNft) => selectedNft.id === nftData.id, + ); + + if (isSelected) { + setNftAuthUser((prevNftAuthUser) => + prevNftAuthUser.filter( + (selectedNft) => selectedNft.id !== nftData.id, + ), + ); } else { - setNftAuthUser([nftData]); + setNftAuthUser((prevNftAuthUser) => [...prevNftAuthUser, nftData]); } } else { - if (nftInputUser.length) { - if (nftInputUser[0].id === nftData.id) setNftInputUser([]); - else setNftInputUser([nftData]); + const isSelected = nftInputUser.some( + (selectedNft) => selectedNft.id === nftData.id, + ); + + if (isSelected) { + setNftInputUser((prevNftInputUser) => + prevNftInputUser.filter( + (selectedNft) => selectedNft.id !== nftData.id, + ), + ); } else { - setNftInputUser([nftData]); + setNftInputUser((prevNftInputUser) => [...prevNftInputUser, nftData]); } } } else if (onClickAction === NftCardActionType.SHOW_NFT_DETAILS) { navigator.clipboard.writeText(JSON.stringify(nftData)); toast.success("NFT data copied to your clipboard"); + } else if (onClickAction === NftCardActionType.NFT_ONCLICK) { } }; @@ -101,14 +137,14 @@ export const NftCard = ({ @@ -122,8 +158,8 @@ export const NftCard = ({ onError={handleImageLoadError} src={nftData.metadata?.image} alt={nftData.metadata?.name} - className="static z-10 w-full overflow-y-auto" - /> + className="static z-10 w-full overflow-y-auto rounded-xl" + />, )} ) : nftData.metadata?.name ? ( @@ -131,7 +167,7 @@ export const NftCard = ({ {ButtonLayout(
{nftData.metadata?.name} -
+
, )} ) : nftData.contract.name && nftData.id.tokenId ? ( @@ -139,7 +175,15 @@ export const NftCard = ({ {ButtonLayout(
{nftData.metadata?.name} -
+
, + )} + + ) : nftData.contractMetadata?.name && nftData.id.tokenId ? ( + <> + {ButtonLayout( +
+ {nftData.contractMetadata?.name} +
, )} ) : null; diff --git a/components/02-molecules/NftsList.tsx b/components/02-molecules/NftsList.tsx index 54cd3267..4c0184c6 100644 --- a/components/02-molecules/NftsList.tsx +++ b/components/02-molecules/NftsList.tsx @@ -1,10 +1,12 @@ -/* eslint-disable react/jsx-key */ -import { NftCard } from "../01-atoms"; import { NFT } from "@/lib/client/constants"; +import { NftCard } from "@/components/02-molecules"; +import { EmptyNftsCards, SwapAddTokenCard } from "@/components/01-atoms"; +/* eslint-disable react/jsx-key */ interface INftsList { nftsList: NFT[]; ownerAddress: string | null; + variant: "your" | "their"; } /** @@ -16,12 +18,29 @@ interface INftsList { * @returns NftsList */ -export const NftsList = ({ nftsList, ownerAddress }: INftsList) => { - return ( -
- {nftsList.map((nft: NFT) => { - return ; - })} +export const NftsList = ({ nftsList, ownerAddress, variant }: INftsList) => { + const emptySquares = EmptyNftsCards(nftsList.length, 15, 30, 30, 30); + const addTokenSquare = SwapAddTokenCard(); + const nftSquares = nftsList.map((nft: NFT, index) => ( +
+
- ); + )); + + if (variant === "your") { + emptySquares.pop(); // Removes the last element to fill with addToken + const allSquares = [...nftSquares, ...emptySquares, addTokenSquare]; + return ( +
+ {allSquares} +
+ ); + } else { + const allSquares = [...nftSquares, ...emptySquares]; + return ( +
+ {allSquares} +
+ ); + } }; diff --git a/components/02-molecules/OfferSummary.tsx b/components/02-molecules/OfferSummary.tsx index 936d65b3..f9a7e72e 100644 --- a/components/02-molecules/OfferSummary.tsx +++ b/components/02-molecules/OfferSummary.tsx @@ -1,8 +1,8 @@ -import { NftCard, SwapContext } from "../01-atoms"; import { EthereumAddress } from "@/lib/shared/types"; import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; -import { ChainInfo } from "@/lib/client/constants"; -import { useEnsName, useNetwork } from "wagmi"; +import { NftCard } from "@/components/02-molecules"; +import { EmptyNftsCards, PersonIcon, SwapContext } from "@/components/01-atoms"; +import { useEnsName } from "wagmi"; import { useContext } from "react"; interface IOfferSummary { @@ -10,20 +10,32 @@ interface IOfferSummary { } export const OfferSummary = ({ forAuthedUser }: IOfferSummary) => { - const { validatedAddressToSwap, nftAuthUser, nftInputUser, destinyChain } = + const { validatedAddressToSwap, nftAuthUser, nftInputUser } = useContext(SwapContext); - const { chain } = useNetwork(); const { data } = useEnsName({ address: validatedAddressToSwap as `0x${string}`, }); const { authenticatedUserAddress } = useAuthenticatedUser(); + const emptySquaresDefault = EmptyNftsCards(0, 4, 8, 12, 12); + const emptySquaresAuthUser = EmptyNftsCards(nftAuthUser.length, 4, 8, 12, 12); + const emptySquaresInputUser = EmptyNftsCards( + nftInputUser.length, + 4, + 8, + 12, + 12, + ); + + const nftUser = forAuthedUser ? nftAuthUser : nftInputUser; return ( -
+
-
+
+ +

{forAuthedUser @@ -34,7 +46,7 @@ export const OfferSummary = ({ forAuthedUser }: IOfferSummary) => { data ? data : new EthereumAddress( - validatedAddressToSwap + validatedAddressToSwap, ).getEllipsedAddress() } gives`}

@@ -42,46 +54,40 @@ export const OfferSummary = ({ forAuthedUser }: IOfferSummary) => {
{!forAuthedUser && !validatedAddressToSwap ? null : (
- {forAuthedUser ? nftAuthUser.length : nftInputUser.length} item - {forAuthedUser - ? nftAuthUser.length !== 1 - ? "s" - : "" - : nftInputUser.length !== 1 - ? "s" - : ""} + {nftUser.length} item + {nftUser.length !== 1 ? "s" : ""}
)}
-
-
+
+
{(forAuthedUser && !authenticatedUserAddress?.address) || (!forAuthedUser && !validatedAddressToSwap) ? null : ( - + <> + {nftUser.map((nft, index) => ( + + ))} + )} -
-
-
-

from

-

- {forAuthedUser ? ( - <>{chain?.name} - ) : ( - <>{ChainInfo[destinyChain].name} - )} -

+ {forAuthedUser + ? emptySquaresAuthUser + : !forAuthedUser + ? emptySquaresInputUser + : emptySquaresDefault} +
); diff --git a/components/02-molecules/ProgressStatus.tsx b/components/02-molecules/ProgressStatus.tsx new file mode 100644 index 00000000..b30dd37d --- /dev/null +++ b/components/02-molecules/ProgressStatus.tsx @@ -0,0 +1,39 @@ +import { SwapContext, ProgressBar } from "@/components/01-atoms"; +import { ADDRESS_ZERO } from "@/lib/client/constants"; +import { useContext, useEffect, useState } from "react"; + +export const ProgressStatus = () => { + const { authedUserSelectedNftsApprovalStatus } = useContext(SwapContext); + + const [ + authedUserSelectedApprovedItemsLength, + setAuthedUserSelectedApprovalLength, + ] = useState(0); + useEffect(() => { + if (authedUserSelectedNftsApprovalStatus.length) { + const approvedNftsCount = authedUserSelectedNftsApprovalStatus.filter( + (item) => item.approved !== ADDRESS_ZERO, + ).length; + + setAuthedUserSelectedApprovalLength(approvedNftsCount); + } + }, [authedUserSelectedNftsApprovalStatus]); + + return ( +
+
+

+ {authedUserSelectedApprovedItemsLength + + "/" + + authedUserSelectedNftsApprovalStatus.length} +

+
+
+ +
+
+ ); +}; diff --git a/components/02-molecules/SwapOffersLayout.tsx b/components/02-molecules/SwapOffersLayout.tsx new file mode 100644 index 00000000..9f7e7c12 --- /dev/null +++ b/components/02-molecules/SwapOffersLayout.tsx @@ -0,0 +1,103 @@ +import { + ErrorIcon, + NoSwapsIcon, + SwapAddManuallyModalLayout, +} from "@/components/01-atoms"; +import { useTheme } from "next-themes"; +import React, { useState } from "react"; +import cc from "classcat"; +import { useRouter } from "next/router"; + +interface EmptyLayoutOffersProps { + icon: React.ReactNode; + title: React.ReactNode; + description: React.ReactNode; + button: React.ReactNode; +} + +const EmptyLayoutOffers = ({ + icon, + title, + description, + button, +}: EmptyLayoutOffersProps) => { + return ( +
+
{icon}
+
+

+ {title} +

+

+ {description} +

+
+
{button}
+
+ ); +}; + +export enum SwapOffersDisplayVariant { + ERROR = "error", + NO_SWAPS_CREATED = "swapless", +} + +type Variant = SwapOffersDisplayVariant | "error" | "swapless"; + +interface SwapOffersLayoutProps { + variant: Variant; +} + +export const SwapOffersLayout = ({ variant }: SwapOffersLayoutProps) => { + const { theme } = useTheme(); + const [toggleManually, setToggleManually] = useState(false); + const router = useRouter(); + + return ( + <> + {variant === SwapOffersDisplayVariant.ERROR ? ( + } + title={<> Sorry, we couldn't load your swaps} + description={<> Please try again later or add your swaps manually} + button={ + <> + + { + setToggleManually(false); + }} + variant="swap" + /> + + } + /> + ) : ( + } + title={<> No swaps here. Let's fill it up!} + description={ +
+ You haven't made or received any proposals. Time to jump in + and start trading. +
+ } + button={ + + } + /> + )} + + ); +}; diff --git a/components/02-molecules/TheHeader.tsx b/components/02-molecules/TheHeader.tsx index d4ba9d79..7e966132 100644 --- a/components/02-molecules/TheHeader.tsx +++ b/components/02-molecules/TheHeader.tsx @@ -1,30 +1,122 @@ -import { ConnectWallet, SwaplaceIcon } from "@/components/01-atoms"; import { useScreenSize } from "@/lib/client/hooks/useScreenSize"; import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { + ConnectWallet, + ENSAvatar, + MoonIcon, + SunIcon, + SwaplaceIcon, + SwappingIcons, + Tooltip, + WalletSidebarTemplate, +} from "@/components/01-atoms"; +import { useSidebar } from "@/lib/client/contexts/SidebarContext.tsx"; import React, { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; +import Link from "next/link"; import cc from "classcat"; export const TheHeader = () => { - const { isDesktop } = useScreenSize(); const { authenticatedUserAddress } = useAuthenticatedUser(); - const [showFullNav, setShowFullNav] = useState( - !isDesktop && !!authenticatedUserAddress?.address - ); + const { toggleSidebar, isSidebarOpen } = useSidebar(); + const { isWideScreen } = useScreenSize(); + const { systemTheme, theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); useEffect(() => { - setShowFullNav(!isDesktop); - }, [isDesktop]); + setMounted(true); + }, []); + + if (!mounted) return null; + const currentTheme = theme === "system" ? systemTheme : theme; + const isDark = currentTheme === "dark"; return ( -
setShowFullNav(true)} - onMouseLeave={() => isDesktop && setShowFullNav(false)} - className="bg-[#F2F2F2] z-40 w-screen h-auto xl:w-auto xl:h-screen py-6 flex xl:flex-col justify-between items-center px-8 font-medium shadow-lg absolute left-0 top-0 xl:items-start" - > - -
- -
-
+ <> +
+
+ + + +
+
+ +
+
+
+ {isDark ? ( + + + + ) : ( + + + + )} +
+
+ {isWideScreen ? ( + <> + {!!authenticatedUserAddress ? ( + + + + ) : ( + +
+ +
+
+ )} + + ) : ( + <> + {!!authenticatedUserAddress ? ( + + + + ) : ( + +
+ +
+
+ )} + + )} +
+
+
+ + ); }; diff --git a/components/02-molecules/TheSidebarHeader.tsx b/components/02-molecules/TheSidebarHeader.tsx new file mode 100644 index 00000000..2a0632b7 --- /dev/null +++ b/components/02-molecules/TheSidebarHeader.tsx @@ -0,0 +1,43 @@ +import { CloseIcon } from "../01-atoms/icons/CloseIcon"; +import { DisconnectWallet } from "../01-atoms/DisconnectWallet"; +import { useSidebar } from "@/lib/client/contexts/SidebarContext.tsx"; +import React from "react"; +import cc from "classcat"; +import { useTheme } from "next-themes"; + +export const TheSidebarHeader = () => { + const { systemTheme, theme } = useTheme(); + const currentTheme = theme === "system" ? systemTheme : theme; + const isDark = currentTheme === "dark"; + const { toggleSidebar } = useSidebar(); + + + return ( +
+ +
+

+ Your wallet +

+ +
+
+ ); +}; diff --git a/components/02-molecules/UserInfo.tsx b/components/02-molecules/UserInfo.tsx new file mode 100644 index 00000000..c2af8dd6 --- /dev/null +++ b/components/02-molecules/UserInfo.tsx @@ -0,0 +1,22 @@ +import { + AccountBalanceWalletSidebar, + EnsNameAndAddressWallet, +} from "@/components/02-molecules"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; +import { useEnsData } from "@/lib/client/hooks/useENSData"; +import React from "react"; + +export const UserInfo = () => { + const { authenticatedUserAddress } = useAuthenticatedUser(); + + useEnsData({ + ensAddress: authenticatedUserAddress, + }); + + return ( +
+ + +
+ ); +}; diff --git a/components/02-molecules/UserOfferInfo.tsx b/components/02-molecules/UserOfferInfo.tsx new file mode 100644 index 00000000..9c3a57c3 --- /dev/null +++ b/components/02-molecules/UserOfferInfo.tsx @@ -0,0 +1,31 @@ +import { ENSAvatar } from "@/components/01-atoms"; +import { useEnsData } from "@/lib/client/hooks/useENSData"; +import { collapseAddress } from "@/lib/client/utils"; +import { EthereumAddress } from "@/lib/shared/types"; + +interface UserOfferInfoProps { + address: EthereumAddress | null; +} + +export const UserOfferInfo = ({ address }: UserOfferInfoProps) => { + const { primaryName } = useEnsData({ + ensAddress: address, + }); + const displayAddress = collapseAddress(address?.toString() ?? "") || ""; + return ( +
+
+
+ {address && } +
+
+ {primaryName ? ( +

{primaryName} gets

+ ) : ( +

{displayAddress} gets

+ )} +
+
+
+ ); +}; diff --git a/components/02-molecules/index.tsx b/components/02-molecules/index.tsx index 9ff6d1dd..29701855 100644 --- a/components/02-molecules/index.tsx +++ b/components/02-molecules/index.tsx @@ -1,6 +1,15 @@ export * from "./CardHome"; -export * from "./TheHeader"; -export * from "../03-organisms/NftsShelf"; +export * from "./CardOffers"; +export * from "./ConfirmSwapModal"; +export * from "./EnsNameAndAddressWallet"; +export * from "./FilterOffers"; +export * from "./NftCard"; export * from "./NftsList"; -export * from "./SwapStation"; export * from "./OfferSummary"; +export * from "./SwapOffersLayout"; +export * from "./ProgressStatus"; +export * from "./TheHeader"; +export * from "./TheSidebarHeader"; +export * from "./UserInfo"; +export * from "./AccountBalanceWalletSidebar"; +export * from "./UserOfferInfo"; diff --git a/components/03-organisms/NftsShelf.tsx b/components/03-organisms/NftsShelf.tsx index ad324413..c4a19590 100644 --- a/components/03-organisms/NftsShelf.tsx +++ b/components/03-organisms/NftsShelf.tsx @@ -1,31 +1,33 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { NftsList } from "../02-molecules"; -import { SwapContext, SwapIcon } from "../01-atoms"; -import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; - import { NFT, ChainInfo, NFTsQueryStatus } from "@/lib/client/constants"; -import { getNftsFrom } from "@/lib/client/blockchain-data"; +import { useAuthenticatedUser } from "@/lib/client/hooks/useAuthenticatedUser"; import { EthereumAddress } from "@/lib/shared/types"; +import { SelectUserIcon, SwapContext } from "@/components/01-atoms"; +import { NftsList } from "@/components/02-molecules"; +import { getNftsFrom } from "@/lib/client/blockchain-data"; import { useContext, useEffect, useState } from "react"; +import { useTheme } from "next-themes"; import { useNetwork } from "wagmi"; +interface INftsShelfProps { + address: string | null; + variant: "your" | "their"; +} + /** * * The Shelf component display the NFTs of given address. + * @param address * * @returns Shelf Nfts based in status of given address */ - -interface INftsShelfProps { - address: string | null; -} - -export const NftsShelf = ({ address }: INftsShelfProps) => { +export const NftsShelf = ({ address, variant }: INftsShelfProps) => { const { chain } = useNetwork(); const [nftsList, setNftsList] = useState(); const [nftsQueryStatus, setNftsQueryStatus] = useState( - NFTsQueryStatus.EMPTY_QUERY + NFTsQueryStatus.EMPTY_QUERY, ); + const { theme } = useTheme(); const { authenticatedUserAddress } = useAuthenticatedUser(); const { validatedAddressToSwap, inputAddress, destinyChain } = @@ -83,42 +85,46 @@ export const NftsShelf = ({ address }: INftsShelfProps) => { }, [validatedAddressToSwap]); return ( -
-
-
- {nftsQueryStatus == NFTsQueryStatus.WITH_RESULTS && nftsList ? ( -
- -
- ) : nftsQueryStatus == NFTsQueryStatus.EMPTY_QUERY || !address ? ( -
-
- -

- Select a user to start swapping -

-
-
- ) : nftsQueryStatus == NFTsQueryStatus.NO_RESULTS ? ( -
-
-

- Given address has no NFTs associated in the given network -

-
-
- ) : nftsQueryStatus == NFTsQueryStatus.LOADING ? ( -
-
-

- Loading NFTs of{" "} - {new EthereumAddress(address).getEllipsedAddress()}... -

-
+
+ {nftsQueryStatus == NFTsQueryStatus.WITH_RESULTS && nftsList ? ( +
+ +
+ ) : nftsQueryStatus == NFTsQueryStatus.EMPTY_QUERY || !address ? ( +
+
+
+
- ) : null} +

+ No user selected yet +

+

+ Search for a user to start swapping items +

+
+
+ ) : nftsQueryStatus == NFTsQueryStatus.NO_RESULTS ? ( +
+
+

+ Given address has no NFTs associated in the given network +

+
+
+ ) : nftsQueryStatus == NFTsQueryStatus.LOADING ? ( +
+
+

+ Loading NFTs of{" "} + {new EthereumAddress(address).getEllipsedAddress()}... +

+
-
+ ) : null}
); }; diff --git a/components/02-molecules/SwapStation.tsx b/components/03-organisms/SwapStation.tsx similarity index 62% rename from components/02-molecules/SwapStation.tsx rename to components/03-organisms/SwapStation.tsx index 5f8361e6..4910fec0 100644 --- a/components/02-molecules/SwapStation.tsx +++ b/components/03-organisms/SwapStation.tsx @@ -1,9 +1,5 @@ -import { OfferSummary } from "@/components/02-molecules"; -import { - ConfirmSwapModal, - PaperPlane, - SwapContext, -} from "@/components/01-atoms"; +import { PaperPlane, SwapContext, SwapExpireTime } from "@/components/01-atoms"; +import { ConfirmSwapModal, OfferSummary } from "@/components/02-molecules"; import { useContext, useEffect, useState } from "react"; import cc from "classcat"; import toast from "react-hot-toast"; @@ -16,7 +12,7 @@ export const SwapStation = () => { useEffect(() => { setIsValidSwap( - !!nftAuthUser.length && !!nftInputUser.length && !!validatedAddressToSwap + !!nftAuthUser.length && !!nftInputUser.length && !!validatedAddressToSwap, ); }, [nftAuthUser, nftInputUser, validatedAddressToSwap]); @@ -37,7 +33,7 @@ export const SwapStation = () => { if (!nftInputUser.length) { toast.error( - "You must select at least one NFT from the destiny wallet to swap" + "You must select at least one NFT from the destiny wallet to swap", ); return; } @@ -47,11 +43,22 @@ export const SwapStation = () => { }; return ( -
+
-

Swap offer

- - +
+
+

+ Swap offer +

+
+
+ +
+
+
+ + +
{