diff --git a/webapps/world-builder-dashboard/constants.ts b/webapps/world-builder-dashboard/constants.ts index dfc0f7c8..a571f7dd 100644 --- a/webapps/world-builder-dashboard/constants.ts +++ b/webapps/world-builder-dashboard/constants.ts @@ -52,6 +52,8 @@ export const L3_NETWORK: HighNetworkInterface = { staker: '0xa6B0461b7E54Fa342Be6320D4938295A81f82Cd3' } +export const ALL_NETWORKS = [L1_NETWORK, L2_NETWORK, L3_NETWORK] + export const L3_NATIVE_TOKEN_SYMBOL = 'TG7T' export const DEFAULT_LOW_NETWORK = L1_NETWORK export const DEFAULT_HIGH_NETWORK = L2_NETWORK diff --git a/webapps/world-builder-dashboard/src/assets/IconChevronDownSelector.tsx b/webapps/world-builder-dashboard/src/assets/IconChevronDownSelector.tsx new file mode 100644 index 00000000..c668f41e --- /dev/null +++ b/webapps/world-builder-dashboard/src/assets/IconChevronDownSelector.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const IconChevronDownSelector: React.FC> = (props) => ( + + + +) + +export default IconChevronDownSelector diff --git a/webapps/world-builder-dashboard/src/assets/IconEthereum.tsx b/webapps/world-builder-dashboard/src/assets/IconEthereum.tsx index 8bf20423..13d56c37 100644 --- a/webapps/world-builder-dashboard/src/assets/IconEthereum.tsx +++ b/webapps/world-builder-dashboard/src/assets/IconEthereum.tsx @@ -1,13 +1,21 @@ import React from 'react' const IconEthereum: React.FC> = (props) => ( - - - - - - - + + + + + + + + + + + + + + + ) diff --git a/webapps/world-builder-dashboard/src/assets/IconFullScreen.tsx b/webapps/world-builder-dashboard/src/assets/IconFullScreen.tsx new file mode 100644 index 00000000..20e979bd --- /dev/null +++ b/webapps/world-builder-dashboard/src/assets/IconFullScreen.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const IconFullScreen: React.FC> = (props) => ( + + + +) + +export default IconFullScreen diff --git a/webapps/world-builder-dashboard/src/assets/IconUSDC.tsx b/webapps/world-builder-dashboard/src/assets/IconUSDC.tsx new file mode 100644 index 00000000..3c97520d --- /dev/null +++ b/webapps/world-builder-dashboard/src/assets/IconUSDC.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const IconUSDC: React.FC> = (props) => ( + + + + + +) + +export default IconUSDC \ No newline at end of file diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.module.css b/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.module.css new file mode 100644 index 00000000..90794672 --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.module.css @@ -0,0 +1,62 @@ + .tokenRow { + display: flex; + justify-content: space-between; + align-items: flex-end; + flex: 1 0 0; + } + + .tokenInformation { + display: flex; + align-items: center; + gap: 8px; + } + + .tokenIconContainer { + display: flex; + width: 40px; + height: 40px; + align-items: flex-start; + } + + .token { + width: 39.583px; + height: 39.583px; + flex-shrink: 0; + } + + .tokenTextContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .tokenTitle { + color: var(--Gray-900, #101828); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 900; + line-height: 24px; /* 171.429% */ + } + + .tokenSymbol { + color: var(--Gray-700, #344054); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + } + + .balanceText { + display: flex; + color: var(--Gray-700, #344054); + + /* Text sm/Regular */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + \ No newline at end of file diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.tsx b/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.tsx new file mode 100644 index 00000000..0f7267c3 --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/tokenRow/TokenRow.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import styles from './TokenRow.module.css' +import { ZERO_ADDRESS } from '@/utils/web3utils' +import useNativeBalance from '@/hooks/useNativeBalance' +import useERC20Balance from '@/hooks/useERC20Balance' +import { useBlockchainContext } from '@/contexts/BlockchainContext' + +interface TokenRowProps { + name: string + address: string + symbol: string + rpc: string + Icon: React.FC> +} + +const useTokenBalance = (address: string, rpc: string, connectedAccount: string | undefined) => { + if (address === ZERO_ADDRESS) { + const { data: balance, isFetching } = useNativeBalance({ + account: connectedAccount, + rpc, + }); + return { balance, isFetching }; + } else { + const { data: balance, isFetching } = useERC20Balance({ + tokenAddress: address, + account: connectedAccount, + rpc, + }); + const formattedBalance = balance?.formatted + return { balance: formattedBalance, isFetching }; + } +}; + + +const TokenRow: React.FC = ({ name, address, symbol, rpc, Icon }) => { + const { connectedAccount } = useBlockchainContext() + const { balance } = useTokenBalance(address, rpc, connectedAccount); + + return ( +
+
+
+ +
+
+
{name}
+
{symbol}
+
+
+
+ {balance ? `${Number(balance).toFixed(4)}` : '0'} +
+
+ ) +} + +export default TokenRow diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.module.css b/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.module.css new file mode 100644 index 00000000..b2b95f89 --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.module.css @@ -0,0 +1,83 @@ +.inputBase { + padding: 0px 0px 0px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + + border-radius: 8px; + border: 1px solid var(--Gray-100, #f2f4f7); + background: var(--Base-White, #fff); + width: 191px !important; + height: 36px; +} + +.inputBase:hover { + background: var(--Gray-25, #fcfcfd); +} + +.inputBaseNetworkName { + color: var(--Gray-500, #344054) !important; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; +} + +/* Mantine modules use !important please */ + +.dropdown { + padding: 0 !important; + background: var(--Base-White, #fff) !important; + border-radius: 8px !important; + border: 1px solid var(--Gray-200, #eaecf0) !important; + background: var(--Base-White, #fff) !important; + box-shadow: + 0 12px 16px -4px rgba(16, 24, 40, 0.08), + 0 4px 6px -2px rgba(16, 24, 40, 0.03) !important; + width: fit-content !important; +} + +.options { + padding: 2px 6px !important; +} + +.option { + height: 36px; + padding: 0 !important; + color: var(--Gray-500, #344054) !important; + font-size: 14px !important; + font-style: normal !important; + font-weight: 500 !important; + line-height: 20px !important; /* 142.857% */ +} + +.option:hover { + background-color: transparent !important; +} + +.optionContainer, +.optionContainerSelected { + height: 36px; + display: flex; + padding: 10px 10px 10px 8px; + align-items: center; + gap: 8px; + flex: 1 0 0; + + border-radius: 6px; +} +.optionContainerSelected { + background: var(--Gray-50, #f9fafb); +} + +.optionLeftSection { + display: flex; + align-items: center; + gap: 8px; + flex: 1 0 0; +} + +.chevron { + stroke: var(--Gray-500, #667085); +} diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.tsx b/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.tsx new file mode 100644 index 00000000..6e898df8 --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/valueSelector/ValueSelector.tsx @@ -0,0 +1,78 @@ +import styles from './ValueSelector.module.css' +import { Combobox, Group, InputBase, InputBaseProps, useCombobox } from 'summon-ui/mantine' +import IconCheck from '@/assets/IconCheck' +import IconChevronDownSelector from '@/assets/IconChevronDownSelector' + +export interface ValueSelect { + valueId: number, + displayName: string, + value: string | undefined +} + +type ValueSelectorProps = { + values: ValueSelect[] + selectedValue: ValueSelect + onChange: (value: ValueSelect) => void +} & InputBaseProps + +const ValueSelector = ({ values, onChange, selectedValue }: ValueSelectorProps) => { + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption() + }) + + return ( + { + const newSelection = values.find((n) => String(n.valueId) === val) + if (newSelection) { + onChange(newSelection) + } + combobox.closeDropdown() + }} + classNames={{ options: styles.options, option: styles.option, dropdown: styles.dropdown }} + > + + 0 ? : ''} + rightSectionPointerEvents='none' + onClick={() => combobox.toggleDropdown()} + > + {selectedValue.displayName} + + + + + {values + .sort((a, b) => { + if (a.valueId === selectedValue.valueId) return 1 + if (b.valueId === selectedValue.valueId) return -1 + return 0 + }) + .map((n) => ( + + +
+
+ {n.displayName} +
+ {n.valueId === selectedValue.valueId && } +
+
+
+ ))} +
+
+
+ ) +} +export default ValueSelector diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.module.css b/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.module.css new file mode 100644 index 00000000..6c78f70c --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.module.css @@ -0,0 +1,129 @@ +.walletButtonContainer { + display: flex; + padding: 8px; + justify-content: space-between; + align-items: center; + align-self: stretch; + border-radius: 8px; + border: 1px solid var(--Gray-200, #eaecf0); + cursor: pointer; +} + +.iconWalletBalance { + display: flex; + width: 172px; + align-items: center; + gap: 12px; +} + +.balance { + display: flex; + align-items: center; + gap: 12px; + flex: 1 0 0; + color: var(--Gray-600, #475467); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ +} + +.iconContainer { + display: flex; + padding: 8px; + justify-content: center; + align-items: center; + gap: 8px; +} + +.iconButton { + stroke: #667085; +} + +.modalTitle { + color: var(--Gray-900, #101828); + font-family: Inter; + font-size: 18px !important; + font-style: normal !important; + font-weight: 600 !important; + line-height: 28px !important; +} + +.modalHeader { + align-items: flex-start; + align-self: stretch; + padding-right: 24px !important; + padding-bottom: 19px !important; +} + +.modalClose { + color: var(--Gray-500, #667085) !important; +} +.modalClose:focus-visible { + outline: none !important; +} +.modalClose:hover { + background-color: transparent !important; + color: var(--Gray-600, #475467) !important; +} + +.modalContent { + display: flex; + padding-bottom: 16px; + flex-direction: column; + align-items: center; + gap: 16px; + align-self: stretch; +} + +.tokensContainer { + display: flex; + flex-direction: column; + gap: 16px; + align-self: stretch; +} + +.footerContainer { + display: flex; + padding: 12px 0px 0px 0px; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; +} + +.gap { + height: 1px; + align-self: stretch; + background: var(--Gray-200, #eaecf0); +} + +.border { + border: 1px solid var(--Gray-100, #f2f4f7); + width: calc(100% + 48px); + margin-left: -24px; +} + +.closeButton { + cursor: pointer; + display: flex; + width: 100px; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + border: 1px solid var(--Gray-300, #d0d5dd); + background: var(--Base-White, #fff); + + /* Shadow/xs */ + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + color: var(--Gray-700, #344054); + + /* Text sm/Semibold */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ +} diff --git a/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.tsx b/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.tsx new file mode 100644 index 00000000..c1dc1cc3 --- /dev/null +++ b/webapps/world-builder-dashboard/src/components/commonComponents/walletButton/WalletButton.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from 'react' +import { ALL_NETWORKS, L3_NETWORK } from '../../../../constants' +import TokenRow from '../tokenRow/TokenRow' +import styles from './WalletButton.module.css' +import { Modal } from 'summon-ui/mantine' +import IconFullScreen from '@/assets/IconFullScreen' +import IconWallet04 from '@/assets/IconWallet04' +import { useBlockchainContext } from '@/contexts/BlockchainContext' +import useNativeBalance from '@/hooks/useNativeBalance' +import { getTokensForNetwork, Token } from '@/utils/tokens' + +interface WalletButtonProps { } + +const WalletButton: React.FC = () => { + const [isModalOpen, setIsModalOpen] = useState(false) + const [tokens, setTokens] = useState([]) + const { connectedAccount, chainId } = useBlockchainContext() + const handleModalClose = () => { + setIsModalOpen(false) + } + + const { data: nativeBalance } = useNativeBalance({ + account: connectedAccount, + rpc: ALL_NETWORKS.find((network) => network.chainId === chainId)?.rpcs[0] || L3_NETWORK.rpcs[0] + }) + + const getTokens = async () => { + const _tokens = getTokensForNetwork(chainId) + setTokens(_tokens) + } + + useEffect(() => { + getTokens() + }, [chainId]) + + return ( + <> +
{ + setIsModalOpen(true) + }} + > +
+ +
+ {nativeBalance + ? `${Number(nativeBalance).toFixed(4)} ${ALL_NETWORKS.find((network) => network.chainId === chainId)?.nativeCurrency?.symbol}` + : 'Fetching...'} +
+
+
+ +
+
+ +
+
+ {tokens.map((token, index) => { + return ( + + + {index !== tokens.length - 1 &&
} + + ) + })} +
+
+
+
+
+ Close +
+
+ + + ) +} + +export default WalletButton diff --git a/webapps/world-builder-dashboard/src/components/faucet/FaucetView.module.css b/webapps/world-builder-dashboard/src/components/faucet/FaucetView.module.css index 6258ff96..443bebe9 100644 --- a/webapps/world-builder-dashboard/src/components/faucet/FaucetView.module.css +++ b/webapps/world-builder-dashboard/src/components/faucet/FaucetView.module.css @@ -1,219 +1,415 @@ .container { - display: flex; - width: 660px; - max-width: 660px; - padding: 19px; - flex-direction: column; - align-items: flex-start; - gap: 20px; + display: flex; + width: 660px; + max-width: 660px; + padding: var(--20px, 20px); + flex-direction: column; + align-items: flex-start; + gap: var(--20px, 20px); + + border-radius: 8px; + border: 1px solid var(--Gray-200, #eaecf0); + background: var(--Gray-50, #f9fafb); +} - border-radius: 8px; - border: 1px solid var(--Gray-200, #EAECF0); - background: var(--Gray-50, #F9FAFB); +.contentContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + align-self: stretch; +} + +.networksContainer { + display: flex; + padding: 11px; + justify-content: center; + align-items: center; + gap: 20px; + align-self: stretch; + + border-radius: 8px; + border: 1px solid var(--Gray-100, #f2f4f7); + background: var(--Base-White, #ffffff); } .header { - display: flex; - flex-direction: column; - gap: 4px; + display: flex; + flex-direction: column; + gap: 4px; } .title { - align-self: stretch; + align-self: stretch; - color: var(--Gray-900, #101828); + color: var(--Gray-900, #101828); - /* Text lg/Semibold */ - font-size: 18px; - font-style: normal; - font-weight: 600; - line-height: 28px; /* 155.556% */ + /* Text lg/Semibold */ + font-size: 18px; + font-style: normal; + font-weight: 600; + line-height: 28px; /* 155.556% */ } .supportingText { - align-self: stretch; - - color: var(--Gray-600, #475467); - - /* Text sm/Regular */ - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 142.857% */ + align-self: stretch; + color: var(--Gray-600, #475467); + /* Text sm/Regular */ + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ } .networksContainer { - padding: 3px; - gap: 8px; - border-radius: 6px; - display: flex; - background: var(--Base-White, #ffffff); - border: 1px solid var(--Gray-100, #f2f4f7); - width: 100%; + padding: 3px; + gap: 8px; + border-radius: 6px; + display: flex; + background: var(--Base-White, #ffffff); + border: 1px solid var(--Gray-100, #f2f4f7); + width: 100%; } - .selectedNetworkButton, .networkButton { - flex: 1; - padding: 8px 12px; - border-radius: 6px; - font-family: Inter, serif; - font-size: 14px; - font-weight: 600; - line-height: 20px; - border: none; - cursor: pointer; - + flex: 1; + padding: 8px 12px; + border-radius: 6px; + font-family: Inter, serif; + font-size: 14px; + font-weight: 600; + line-height: 20px; + border: none; + cursor: pointer; } .selectedNetworkButton { - background: var(--Primary-600, #EF233B); - color: var(--Base-White, #ffffff); - transition: transform 0.4s ease-out, color, background-color 0.4s ease; - + background: var(--Primary-600, #ef233b); + color: var(--Base-White, #ffffff); + transition: + transform 0.4s ease-out, + color, + background-color 0.4s ease; } .networkButton { - background: transparent; - color: var(--Gray-600, #475467); + background: transparent; + color: var(--Gray-600, #475467); } .addressContainer { - display: flex; - flex-direction: column; - gap: 6px; - align-self: stretch; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + flex: 1 0 0; } -.label { - color: var(--Gray-600, #475467); +.addressSelectorContainer { + display: flex; + align-items: flex-end; + gap: 16px; + align-self: stretch; +} - /* Text xs/Medium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 18px; /* 150% */ +.selectorContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + align-self: stretch; + height: 100%; } -.address, .addressPlaceholder { - display: flex; - padding: 7px 11px; - align-items: center; - gap: 8px; - align-self: stretch; +.selectorWrapper { + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 8px; + border: 1px solid var(--Gray-100, #f2f4f7); + background: var(--Base-White, #fff); +} - border-radius: 8px; - border: 1px solid var(--Gray-100, #F2F4F7); - background: var(--Base-White, #FFF); - color: var(--Gray-500, #667085); +.selector { + flex: 1 0 0; + color: var(--Gray-500, #667085); - /* Text sm/Regular */ - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 142.857% */ + /* Text sm/Regular */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ } -.address { - color: var(--Gray-700, #344054); +.selectorChevron { + width: 16px; + height: 16px; } -.hintBadge { - display: flex; - padding: 2px 8px; - justify-content: center; - align-items: center; - align-self: stretch; +.textSeparator { + display: flex; + width: 16px; + height: 36px; + flex-direction: column; + justify-content: center; + color: var(--Gray-600, #475467); + /* Text sm/Regular */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ +} - border-radius: 16px; - background: var(--Blue-50, #EFF8FF); - mix-blend-mode: var(--mix-blend, multiply); - color: var(--Blue-700, #175CD3); - text-align: center; +.connectWalletButton { + cursor: pointer; + display: flex; + height: 36px; + padding: 8px var(--14px, 14px); + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + border: 1px solid var(--Primary-50, #ffebef); + background: var(--Primary-50, #ffebef); +} + +.connectWalletText { + color: var(--Primary-700, #dd1534); - /* Text xs/Medium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 18px; /* 150% */ - user-select: none; + /* Text sm/Semibold */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ } -.button { - display: flex; - padding: 10px 16px; - justify-content: center; - align-items: center; - gap: 8px; - align-self: stretch; +.requestTokensButton { + cursor: pointer; + display: flex; + height: 36px; + padding: 8px var(--14px, 14px); + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + border: 1px solid var(--Primary-600, #ef233b); + background: var(--Primary-600, #ef233b); + + /* Shadow/xs */ + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); +} - border-radius: 8px; - border: 1px solid var(--Primary-600, #EF233B); - background: var(--Primary-600, #EF233B); +.requestTokensButtonDisabled { + display: flex; + height: 36px; + padding: 8px var(--14px, 14px); + justify-content: center; + align-items: center; + gap: 8px; + cursor: not-allowed; + border-radius: 8px; + background: var(--Gray-600, #475467); + + /* Shadow/xs */ + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + border: none; +} - /* Shadow/xs */ - box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); - color: var(--Base-White, #FFF); +.requestTokensButtonText { + color: var(--Base-White, #fff); - /* Text sm/Semibold */ - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: 20px; /* 142.857% */ + /* Text sm/Semibold */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ } -.button:disabled { - cursor: not-allowed; - background: var(--Gray-600, #475467); - box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); - border: none; +.label { + color: var(--Gray-600, #475467); + + /* Text xs/Medium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 150% */ } -.warningContainer, .errorContainer { - text-align: center; - width: 100%; - justify-content: center; - padding: 2px 8px; - align-items: center; - gap: 4px; +.address, +.addressPlaceholder { + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + height: 36px; + + border-radius: 8px; + border: 1px solid var(--Gray-100, #f2f4f7); + background: var(--Base-White, #fff); + color: var(--Gray-700, var(--color-gray-Gray-700, #344054)); + + /* Text sm/Regular */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ +} - border-radius: 16px; - background: var(--Warning-50, #FFFAEB); - mix-blend-mode: var(--mix-blend, multiply); - color: var(--Warning-700, #B54708); +.address { + color: var(--Gray-700, #344054); +} - /* Text xs/Medium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 18px; /* 150% */ +.address:focus { + outline: none; + box-shadow: none; } +.hintBadge { + display: flex; + padding: 2px 8px; + justify-content: center; + align-items: center; + align-self: stretch; + + border-radius: 16px; + background: var(--Blue-50, #eff8ff); + mix-blend-mode: var(--mix-blend, multiply); + color: var(--Blue-700, #175cd3); + text-align: center; + + /* Text xs/Medium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 150% */ + user-select: none; +} + +.button { + display: flex; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 8px; + align-self: stretch; + + border-radius: 8px; + border: 1px solid var(--Primary-600, #ef233b); + background: var(--Primary-600, #ef233b); + + /* Shadow/xs */ + box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + color: var(--Base-White, #fff); + + /* Text sm/Semibold */ + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ +} + +.button:disabled { + cursor: not-allowed; + background: var(--Gray-600, #475467); + box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + border: none; +} + +.warningContainer, .errorContainer { - border-radius: 16px; - background: var(--Error-50, #FEF3F2); - mix-blend-mode: var(--mix-blend, multiply); - color: var(--Error-700, #B42318); + text-align: center; + width: 100%; + justify-content: center; + padding: 2px 8px; + align-items: center; + gap: 4px; + + border-radius: 16px; + background: var(--Warning-50, #fffaeb); + mix-blend-mode: var(--mix-blend, multiply); + color: var(--Warning-700, #b54708); + + /* Text xs/Medium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 150% */ +} + +.errorContainer { + border-radius: 16px; + background: var(--Error-50, #fef3f2); + mix-blend-mode: var(--mix-blend, multiply); + color: var(--Error-700, #b42318); } .time { - display: inline-block; - min-width: 11ch; - white-space: nowrap; - font-weight: 500; - text-align: start; + display: inline-block; + min-width: 11ch; + white-space: nowrap; + font-weight: 500; + text-align: start; +} + +.gap { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + align-self: stretch; } +.gapBetween { + width: 341px; + height: 1px; + background: #d0d5dd; +} @media (max-width: 1199px) { - .container { - width: 100vw; - max-width: 100vw; - border: none; - border-top: 1px solid var(--Gray-200, #EAECF0); - border-bottom: 1px solid var(--Gray-200, #EAECF0); - background: var(--Gray-50, #F9FAFB); - border-radius: 0; - } + .container { + display: flex; + padding: 16px; + flex-direction: column; + align-items: flex-start; + gap: var(--20px, 20px); + align-self: stretch; + border-radius: 8px; + border: 1px solid var(--Gray-200, #eaecf0); + background: var(--Gray-50, #f9fafb); + width: 100%; + } + + .contentContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + align-self: stretch; + } + + .addressSelectorContainer { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + gap: 12px; + align-self: stretch; + } + + .address { + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + } } diff --git a/webapps/world-builder-dashboard/src/components/faucet/FaucetView.tsx b/webapps/world-builder-dashboard/src/components/faucet/FaucetView.tsx index 04fdbf11..d3ff9b07 100644 --- a/webapps/world-builder-dashboard/src/components/faucet/FaucetView.tsx +++ b/webapps/world-builder-dashboard/src/components/faucet/FaucetView.tsx @@ -1,94 +1,98 @@ import React, { useEffect, useState } from 'react' import { useMutation, useQuery, useQueryClient } from 'react-query' import { + ALL_NETWORKS, FAUCET_CHAIN, - G7T_FAUCET_ADDRESS, - L1_NETWORK, - L2_NETWORK, L3_NATIVE_TOKEN_SYMBOL, L3_NETWORK } from '../../../constants' import styles from './FaucetView.module.css' -import { ethers } from 'ethers' import { NetworkInterface, useBlockchainContext } from '@/contexts/BlockchainContext' import { useBridgeNotificationsContext } from '@/contexts/BridgeNotificationsContext' import { useUISettings } from '@/contexts/UISettingsContext' +import { useFaucetAPI } from '@/hooks/useFaucetAPI' import { TransactionRecord } from '@/utils/bridge/depositERC20ArbitrumSDK' import { timeDifferenceInHoursAndMinutes, timeDifferenceInHoursMinutesAndSeconds } from '@/utils/timeFormat' -import { faucetABI } from '@/web3/ABI/faucet_abi' -import { Signer } from '@ethersproject/abstract-signer' -import { useMediaQuery } from '@mantine/hooks' +import { ZERO_ADDRESS } from '@/utils/web3utils' +import ValueSelector, { ValueSelect } from '../commonComponents/valueSelector/ValueSelector' -interface FaucetViewProps {} -const FaucetView: React.FC = ({}) => { +interface FaucetViewProps { } +const FaucetView: React.FC = ({ }) => { + const [selectedAccountType, setSelectedAccountType] = useState({ valueId: 0, displayName: 'External Address', value: '' }) + const [address, setAddress] = useState('') const [selectedNetwork, setSelectedNetwork] = useState(L3_NETWORK) - const { connectedAccount, isConnecting, getProvider, connectWallet } = useBlockchainContext() + const { useFaucetInterval, useFaucetTimestamp } = useFaucetAPI() + const { connectedAccount, connectWallet, chainId } = useBlockchainContext() const [animatedInterval, setAnimatedInterval] = useState('') const [nextClaimTimestamp, setNextClaimTimestamp] = useState(0) const [networkError, setNetworkError] = useState('') const { faucetTargetChainId } = useUISettings() + const { refetchNewNotifications } = useBridgeNotificationsContext() - const { refetchNewNotifications } = useBridgeNotificationsContext() - const smallView = useMediaQuery('(max-width: 1199px)') + const values = [ + { + valueId: 0, + displayName: `External Address`, + value: '' + }, + { + valueId: 1, + displayName: `Connected Account`, + value: connectedAccount + } + ] useEffect(() => { - const targetNetwork = [L1_NETWORK, L2_NETWORK, L3_NETWORK].find((n) => n.chainId === faucetTargetChainId) + const targetNetwork = ALL_NETWORKS.find((n) => n.chainId === faucetTargetChainId) if (targetNetwork) { setSelectedNetwork(targetNetwork) } - }, [faucetTargetChainId]) - const handleClick = async () => { - if (!connectedAccount) { - await connectWallet() - return - } - const provider = await getProvider(L2_NETWORK) - const signer = provider.getSigner() - claim.mutate({ isL2Target: selectedNetwork.chainId === L2_NETWORK.chainId, signer }) + if (selectedAccountType.valueId === 0 || !connectedAccount) setAddress('') + else setAddress(connectedAccount) + }, [faucetTargetChainId, selectedAccountType, connectedAccount]) + + useEffect(() => { + setNetworkError('') + }, [connectedAccount]) + + const handleConnect = async () => { + if (!connectedAccount) connectWallet() + } + + const handleSelectAccountType = (selectedAccountType: ValueSelect) => { + if (selectedAccountType.valueId === 0 && !connectedAccount) setAddress('') + else setAddress(selectedAccountType.value) + setSelectedAccountType(selectedAccountType) + setNetworkError('') } const queryClient = useQueryClient() const claim = useMutation( - async ({ isL2Target, signer }: { isL2Target: boolean; signer: Signer }) => { + async ({ address }: { isL2Target: boolean; address: string | undefined }) => { + const res = await fetch(`https://api.game7.build/faucet/request/${address}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + if (!res.ok) { + throw new Error(`Error: ${res.statusText}`) + } setNetworkError('') - if (window.ethereum) { - const contractAbi = [ - { - inputs: [], - name: 'claim', - outputs: [], - stateMutability: 'nonpayable', - type: 'function' - }, - { - inputs: [], - name: 'claimL3', - outputs: [], - stateMutability: 'nonpayable', - type: 'function' - } - ] - - const contract = new ethers.Contract(G7T_FAUCET_ADDRESS, contractAbi, signer) - const tx = isL2Target ? await contract.claim() : await contract.claimL3() - const receipt = await tx.wait() // Wait for the transaction to be mined - const type: 'CLAIM' | 'DEPOSIT' | 'WITHDRAWAL' = 'CLAIM' - return { - type, - amount: '1', - highNetworkChainId: selectedNetwork.chainId, - lowNetworkChainId: FAUCET_CHAIN.chainId, - lowNetworkHash: receipt.transactionHash, - lowNetworkTimestamp: Date.now() / 1000, - completionTimestamp: Date.now() / 1000, - newTransaction: true - } + const type: 'CLAIM' | 'DEPOSIT' | 'WITHDRAWAL' = 'CLAIM' + return { + type, + amount: '1', + highNetworkChainId: selectedNetwork.chainId, + lowNetworkChainId: FAUCET_CHAIN.chainId, + lowNetworkTimestamp: Date.now() / 1000, + completionTimestamp: Date.now() / 1000, + newTransaction: true } - throw new Error('no window.ethereum') }, { - onSuccess: (data: TransactionRecord | undefined, variables) => { + onSuccess: (data: TransactionRecord | undefined, { address }) => { try { const transactionsString = localStorage.getItem(`bridge-${connectedAccount}-transactions`) @@ -101,82 +105,80 @@ const FaucetView: React.FC = ({}) => { } catch (e) { console.log(e) } - queryClient.setQueryData(['nextFaucetClaimTimestamp', connectedAccount], (oldData: any) => { - const lastClaimTimestamp = Date.now() / 1000 - if (!oldData) { - queryClient.refetchQueries(['nextFaucetClaimTimestamp']) - return oldData - } + const lastClaimTimestamp = Date.now() / 1000 + const faucetInterval = faucetIntervalQuery.data ? Number(faucetIntervalQuery.data) : 0 + const nextClaimL3Timestamp = lastClaimTimestamp + faucetInterval - const nextClaimTimestamp = lastClaimTimestamp + oldData.faucetTimeInterval - const interval = timeDifferenceInHoursAndMinutes(Date.now() / 1000, nextClaimTimestamp) - const isAvailable = false - const L2 = variables.isL2Target ? { nextClaimTimestamp, interval, isAvailable } : oldData.L2 - const L3 = !variables.isL2Target ? { nextClaimTimestamp, interval, isAvailable } : oldData.L3 + const intervalL3 = timeDifferenceInHoursAndMinutes(Date.now() / 1000, nextClaimL3Timestamp) + const isAvailableL3 = compareTimestampWithCurrentMoment(nextClaimL3Timestamp) - return { faucetTimeInterval: oldData.faucetTimeInterval, L2, L3 } + const updatedL3 = { + interval: intervalL3, + nextClaimTimestamp: nextClaimL3Timestamp, + isAvailable: isAvailableL3, + } + queryClient.setQueryData(['nextFaucetClaimTimestamp', address], (oldData: any) => { + if (oldData) { + return { + ...oldData, + L3: updatedL3, // Update the L3 data + } + } + return { faucetTimeInterval: faucetInterval, L3: updatedL3 } }) + + queryClient.invalidateQueries(['nextFaucetClaimTimestamp', address]) queryClient.refetchQueries('pendingTransactions') queryClient.refetchQueries(['notifications']) queryClient.refetchQueries(['nativeBalance']) queryClient.refetchQueries(['ERC20balance']) - refetchNewNotifications(connectedAccount ?? '') + refetchNewNotifications(address ?? '') + }, + onError: (error) => { + setNetworkError('Something went wrong') + console.log(error) + console.error("Error requesting tokens:", error) }, - onError: (e: Error) => { - setNetworkError('Something went wrong. Try again, please') - console.error('Transaction failed:', e) - console.log(e) - } } ) function compareTimestampWithCurrentMoment(unixTimestamp: number): boolean { - const timestampInMillis = unixTimestamp * 1000 // Unix timestamp in milliseconds - const currentInMillis = Date.now() // Current time in milliseconds + const timestampInMillis = unixTimestamp * 1000 + const currentInMillis = Date.now() return timestampInMillis <= currentInMillis } + const lastClaimedTimestampQuery = useFaucetTimestamp(address) + const faucetIntervalQuery = useFaucetInterval() + const nextClaimAvailable = useQuery( - ['nextFaucetClaimTimestamp', connectedAccount], + ['nextFaucetClaimTimestamp', address], async () => { - const rpc = L2_NETWORK.rpcs[0] - const provider = new ethers.providers.JsonRpcProvider(rpc) - const faucetContract = new ethers.Contract(G7T_FAUCET_ADDRESS, faucetABI, provider) - - const lastClaimedL2Timestamp = Number(await faucetContract.lastClaimedL2Timestamp(connectedAccount)) - const lastClaimedL3Timestamp = Number(await faucetContract.lastClaimedL3Timestamp(connectedAccount)) - - const faucetTimeInterval = Number(await faucetContract.faucetTimeInterval()) - const nextClaimL2Timestamp = lastClaimedL2Timestamp + faucetTimeInterval + const lastClaimedL3Timestamp = Number(lastClaimedTimestampQuery.data) + const faucetTimeInterval = Number(faucetIntervalQuery.data) const nextClaimL3Timestamp = lastClaimedL3Timestamp + faucetTimeInterval - const intervalL2 = timeDifferenceInHoursAndMinutes(Date.now() / 1000, nextClaimL2Timestamp) const intervalL3 = timeDifferenceInHoursAndMinutes(Date.now() / 1000, nextClaimL3Timestamp) - - const isAvailableL2 = compareTimestampWithCurrentMoment(nextClaimL2Timestamp) const isAvailableL3 = compareTimestampWithCurrentMoment(nextClaimL3Timestamp) - const L2 = { interval: intervalL2, nextClaimTimestamp: nextClaimL2Timestamp, isAvailable: isAvailableL2 } const L3 = { interval: intervalL3, nextClaimTimestamp: nextClaimL3Timestamp, isAvailable: isAvailableL3 } - - return { faucetTimeInterval, L2, L3 } + return { faucetTimeInterval, L3 } }, { - enabled: !!connectedAccount + enabled: !!address && + !!lastClaimedTimestampQuery.data && + !!faucetIntervalQuery.data } ) useEffect(() => { - if (!nextClaimAvailable.data) { - return - } - const intervalInfo = - selectedNetwork.chainId === L2_NETWORK.chainId ? nextClaimAvailable.data.L2 : nextClaimAvailable.data.L3 + if (!nextClaimAvailable.data) return + const intervalInfo = nextClaimAvailable.data.L3 if (!intervalInfo.isAvailable) { setNextClaimTimestamp(intervalInfo.nextClaimTimestamp) } - }, [nextClaimAvailable.data, selectedNetwork]) + }, [nextClaimAvailable.data, chainId]) useEffect(() => { let intervalId: NodeJS.Timeout @@ -198,56 +200,65 @@ const FaucetView: React.FC = ({}) => {
Testnet Faucet
- {`Request 1 ${L3_NATIVE_TOKEN_SYMBOL} per day to your wallet address.`} + Request and get 1{L3_NATIVE_TOKEN_SYMBOL} testnet token to your connected wallet or an external address on G7 network.
-
- +
+
+
+
Recipient Address
+ { + setAddress(e.target.value) + }} + /> +
+ {!connectedAccount ? ( + <> +
+ Or +
+
{ handleConnect() }}> +
+ Connect Wallet +
+
+ + ) : ( +
+
Account
+ +
+ )} +
-
-
-
Connected Wallet Address
- {connectedAccount ? ( -
- {smallView ? `${connectedAccount.slice(0, 6)}....${connectedAccount.slice(-4)}` : connectedAccount} +
+ {claim.isLoading ? `Requesting...` : `Request Tokens`}
- ) : ( -
Please connect a wallet...
- )} +
{!!networkError &&
{networkError}.
} {!networkError && nextClaimAvailable.isLoading && (
Checking faucet permissions...
)} - - {!nextClaimAvailable.isLoading && - !networkError && - (selectedNetwork.chainId === L2_NETWORK.chainId - ? nextClaimAvailable.data?.L2.isAvailable - : nextClaimAvailable.data?.L3.isAvailable) && ( -
You may only request funds to a connected wallet.
- )} - {!nextClaimAvailable.isLoading && !connectedAccount && !networkError && ( -
You may only request funds to a connected wallet.
- )} - {selectedNetwork.chainId === L2_NETWORK.chainId && - nextClaimAvailable.data && - !nextClaimAvailable.data.L2.isAvailable && ( -
- {`You requested funds recently. Come back in `} - {animatedInterval} -
- )} {selectedNetwork.chainId === L3_NETWORK.chainId && nextClaimAvailable.data && !nextClaimAvailable.data.L3.isAvailable && ( @@ -256,26 +267,8 @@ const FaucetView: React.FC = ({}) => { {` ${animatedInterval}`}
)} -
) } + export default FaucetView diff --git a/webapps/world-builder-dashboard/src/contexts/BlockchainContext.tsx b/webapps/world-builder-dashboard/src/contexts/BlockchainContext.tsx index 4a38ae3c..ca9b001e 100644 --- a/webapps/world-builder-dashboard/src/contexts/BlockchainContext.tsx +++ b/webapps/world-builder-dashboard/src/contexts/BlockchainContext.tsx @@ -16,6 +16,9 @@ interface BlockchainContextType { setSelectedHighNetwork: (network: NetworkInterface) => void isMetaMask: boolean getProvider: (network: NetworkInterface) => Promise + accounts: string[] + setAccounts: (accounts: string[]) => void + chainId: number | undefined isConnecting: boolean } @@ -57,8 +60,9 @@ export const BlockchainProvider: React.FC = ({ children const [selectedHighNetwork, _setSelectedHighNetwork] = useState(DEFAULT_HIGH_NETWORK) const [isMetaMask, setIsMetaMask] = useState(false) const [isConnecting, setIsConnecting] = useState(false) - + const [chainId, setChainId] = useState(undefined) const [connectedAccount, setConnectedAccount] = useState() + const [accounts, setAccounts] = useState(['']) const tokenAddress = '0x5f88d811246222F6CB54266C42cc1310510b9feA' const setSelectedLowNetwork = (network: NetworkInterface) => { @@ -93,6 +97,23 @@ export const BlockchainProvider: React.FC = ({ children } }, [window.ethereum]) + const fetchChainId = async () => { + const _chainId = (await walletProvider?.getNetwork())?.chainId + setChainId(_chainId) + } + + const handleChainChanged = (hexedChainId: string) => { + const newChainId = parseInt(hexedChainId, 16) // Convert hex chainId to decimal + setChainId(newChainId) + } + + useEffect(() => { + fetchChainId() + if (window.ethereum?.on) { + window.ethereum.on('chainChanged', handleChainChanged) + } + }, [walletProvider]) + const handleAccountsChanged = async () => { const ethereum = window.ethereum if (ethereum) { @@ -100,6 +121,7 @@ export const BlockchainProvider: React.FC = ({ children // @ts-ignore setIsMetaMask(window.ethereum?.isMetaMask && !window.ethereum?.overrideIsMetaMask) const accounts = await provider.listAccounts() + setAccounts(accounts) if (accounts.length > 0) { setConnectedAccount(accounts[0]) } else { @@ -228,9 +250,12 @@ export const BlockchainProvider: React.FC = ({ children selectedHighNetwork, setSelectedHighNetwork, isMetaMask, + chainId, disconnectWallet, getProvider, - isConnecting + isConnecting, + accounts, + setAccounts }} > {children} diff --git a/webapps/world-builder-dashboard/src/hooks/useFaucetAPI.ts b/webapps/world-builder-dashboard/src/hooks/useFaucetAPI.ts new file mode 100644 index 00000000..9fd10ee3 --- /dev/null +++ b/webapps/world-builder-dashboard/src/hooks/useFaucetAPI.ts @@ -0,0 +1,72 @@ +import { useQuery } from 'react-query' + +const BASE_URL = 'https://api.game7.build' + +export const useFaucetAPI = () => { + + const useFaucetTimestamp = (address: string | undefined) => { + return useQuery( + ['faucetTimestamp', address], + async () => { + const res = await fetch(`${BASE_URL}/faucet/timestamp/${address}`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error(`Error: ${res.statusText}`); + } + const data = await res.json(); + return data.result; + }, + { + enabled: !!address, + retry: false, + } + ); + }; + + + const useFaucetInterval = () => { + return useQuery( + 'faucetInterval', + async () => { + const res = await fetch(`${BASE_URL}/faucet/interval`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error(`Error: ${res.statusText}`); + } + const data = await res.json(); + return data.result; + }, + { + retry: false, + } + ); + }; + + const useFaucetCountdown = (address: string) => { + return useQuery( + ['faucetCountdown', address], + async () => { + const res = await fetch(`${BASE_URL}/faucet/countdown/${address}`, { + method: 'GET', + }); + if (!res.ok) { + throw new Error(`Error: ${res.statusText}`); + } + const data = await res.json(); + return data.result; + }, + { + enabled: !!address, + retry: false, + } + ); + }; + + return { + useFaucetTimestamp, + useFaucetInterval, + useFaucetCountdown, + } +} diff --git a/webapps/world-builder-dashboard/src/layouts/MainLayout/DesktopSidebar.tsx b/webapps/world-builder-dashboard/src/layouts/MainLayout/DesktopSidebar.tsx index c13d9993..0af0bdfc 100644 --- a/webapps/world-builder-dashboard/src/layouts/MainLayout/DesktopSidebar.tsx +++ b/webapps/world-builder-dashboard/src/layouts/MainLayout/DesktopSidebar.tsx @@ -1,7 +1,9 @@ import React, { ReactNode } from 'react' import { useLocation, useNavigate } from 'react-router-dom' +import { ALL_NETWORKS } from '../../../constants' import styles from './MainLayout.module.css' import IconLogout from '@/assets/IconLogout' +import WalletButton from '@/components/commonComponents/walletButton/WalletButton' import { useBlockchainContext } from '@/contexts/BlockchainContext' import Game7Logo from '@/layouts/MainLayout/Game7Logo' @@ -11,7 +13,8 @@ interface DesktopSidebarProps { const DesktopSidebar: React.FC = ({ navigationItems }) => { const location = useLocation() const navigate = useNavigate() - const { connectedAccount, isMetaMask, disconnectWallet } = useBlockchainContext() + const { connectedAccount, isMetaMask, connectWallet, disconnectWallet, chainId, isConnecting } = + useBlockchainContext() return (
@@ -31,12 +34,24 @@ const DesktopSidebar: React.FC = ({ navigationItems }) => {
- {connectedAccount && ( -
-
{`${connectedAccount.slice(0, 6)}...${connectedAccount.slice(-4)}`}
- {isMetaMask && } + {connectedAccount ? ( + <> + {/* If network not found, hide */} + {ALL_NETWORKS.find((network) => network.chainId === chainId) ? : <>} +
+
+ {`${connectedAccount.slice(0, 6)}...${connectedAccount.slice(-4)}`} +
+ {isMetaMask && } +
+ + ) : ( +
+ {isConnecting ? ( +
{'Connecting Wallet...'}
+ ) : ( +
{'Connect Wallet'}
+ )}
)}
diff --git a/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.module.css b/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.module.css index 3ff9cec3..baaa3a58 100644 --- a/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.module.css +++ b/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.module.css @@ -1,131 +1,168 @@ .container { - display: flex; - height: 100vh; - max-height: 100vh; - width: 100vw; - position: relative; - font-family: Inter, sans-serif; + display: flex; + height: 100vh; + max-height: 100vh; + width: 100vw; + position: relative; + font-family: Inter, sans-serif; } .sideBar { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - flex: 1 0 0; - align-self: stretch; - max-width: 280px; - min-width: 280px; - border-right: 1px solid var(--Gray-200, #EAECF0); - background: var(--Base-White, #FFF); + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + flex: 1 0 0; + align-self: stretch; + max-width: 280px; + min-width: 280px; + border-right: 1px solid var(--Gray-200, #eaecf0); + background: var(--Base-White, #fff); } .sideBarTop { - display: flex; - padding-top: 32px; - flex-direction: column; - align-items: flex-start; - gap: 24px; - align-self: stretch; + display: flex; + padding-top: 32px; + flex-direction: column; + align-items: flex-start; + gap: 24px; + align-self: stretch; } .logoContainer { - display: flex; - padding: 0 20px 0 24px; - align-items: center; - align-self: stretch; + display: flex; + padding: 0 20px 0 24px; + align-items: center; + align-self: stretch; } .logoWrapper { - display: flex; - align-items: center; - gap: 12px; + display: flex; + align-items: center; + gap: 12px; } .navigation { - display: flex; - padding: 0 16px; - flex-direction: column; - align-items: flex-start; - gap: 4px; - align-self: stretch; - background: var(--Base-White, #FFF); + display: flex; + padding: 0 16px; + flex-direction: column; + align-items: flex-start; + gap: 4px; + align-self: stretch; + background: var(--Base-White, #fff); } -.navButton, .selectedNavButton { - display: flex; - padding: 8px 12px; - align-items: center; - gap: 12px; - align-self: stretch; - - border-radius: 6px; - background: var(--Gray-50, #F9FAFB); - color: var(--Gray-900, #101828); - - /* Text md/Semibold */ - font-size: 16px; - font-style: normal; - font-weight: 600; - line-height: 24px; /* 150% */ - - text-transform: capitalize; +.navButton, +.selectedNavButton { + display: flex; + padding: 8px 12px; + align-items: center; + gap: 12px; + align-self: stretch; + + border-radius: 6px; + background: var(--Gray-50, #f9fafb); + color: var(--Gray-900, #101828); + + /* Text md/Semibold */ + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; /* 150% */ + + text-transform: capitalize; } .navButton { - background: var(--Base-White, #FFF); - cursor: pointer; + background: var(--Base-White, #fff); + cursor: pointer; } .footer { - display: flex; - padding: 0 15px 32px 16px; - flex-direction: column; - align-items: flex-start; - gap: 24px; - align-self: stretch; + display: flex; + padding: 0 15px 32px 16px; + flex-direction: column; + align-items: flex-start; + gap: 24px; + align-self: stretch; } .web3AddressContainer { - display: flex; - padding: 24px 8px 0 8px; - align-items: center; - gap: 47px; - align-self: stretch; - justify-content: space-between; - - border-top: 1px solid var(--Gray-200, #EAECF0); - height: 60px; + display: flex; + padding: 24px 8px 0 8px; + align-items: center; + gap: 47px; + align-self: stretch; + justify-content: space-between; + + border-top: 1px solid var(--Gray-200, #eaecf0); + height: 60px; } .web3address { - display: flex; - color: var(--Gray-600, #475467); - font-family: Inter, sans-serif; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; /* 142.857% */ + display: flex; + color: var(--Gray-600, #475467); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ } .iconButton { - cursor: pointer; - stroke: #667085; - margin-right: 8px; + cursor: pointer; + stroke: #667085; + margin-right: 8px; } .iconButton:hover { - stroke: var(--Gray-600, #475467); + stroke: var(--Gray-600, #475467); } - @media (max-width: 1199px) { - .container { - display: flex; - flex-direction: column; - min-width: 100vw; - } - .logoContainer { - padding: 0; - } + .container { + display: flex; + flex-direction: column; + min-width: 100vw; + } + .logoContainer { + padding: 0; + } } + +.connectWalletButton { + cursor: pointer; + display: flex; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 8px; + flex: 1 0 0; + border-radius: 8px; + border: 1px solid var(--Primary-600, #ef233b); + background: var(--Primary-600, #ef233b); + + /* Shadow/xs */ + box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + width: 100%; +} + +.connectWalletText { + color: var(--Base-White, #fff); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ +} + + +.connectingWalletText { + color: var(--Base-White, #fff); + opacity: 0.7; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ +} \ No newline at end of file diff --git a/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.tsx b/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.tsx index 24807c3c..d2e7b0d2 100644 --- a/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.tsx +++ b/webapps/world-builder-dashboard/src/layouts/MainLayout/MainLayout.tsx @@ -1,10 +1,12 @@ // React and related libraries import React from 'react' import { Outlet } from 'react-router-dom' + // Styles import styles from './MainLayout.module.css' import IconDroplets02 from '@/assets/IconDroplets02' import IconWallet04 from '@/assets/IconWallet04' + // Local components and assets import DesktopSidebar from '@/layouts/MainLayout/DesktopSidebar' import MobileSidebar from '@/layouts/MainLayout/MobileSidebar' @@ -14,8 +16,9 @@ interface MainLayoutProps {} const NAVIGATION_ITEMS = [ { name: 'bridge', navigateTo: '/bridge', icon: }, - { name: 'faucet', navigateTo: '/faucet', icon: }, + { name: 'faucet', navigateTo: '/faucet', icon: } ] + const MainLayout: React.FC = ({}) => { const smallView = useMediaQuery('(max-width: 1199px)') return ( diff --git a/webapps/world-builder-dashboard/src/pages/BridgePage/BridgePage.tsx b/webapps/world-builder-dashboard/src/pages/BridgePage/BridgePage.tsx index 58949a03..a3cb42c3 100644 --- a/webapps/world-builder-dashboard/src/pages/BridgePage/BridgePage.tsx +++ b/webapps/world-builder-dashboard/src/pages/BridgePage/BridgePage.tsx @@ -32,7 +32,6 @@ const BridgePage = () => { const notifications = useNotifications(connectedAccount, notificationsOffset, notificationsLimit) const { newNotifications, refetchNewNotifications } = useBridgeNotificationsContext() const smallView = useMediaQuery('(max-width: 1199px)') - const queryClient = useQueryClient() useEffect(() => { diff --git a/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.module.css b/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.module.css index d37bc89d..554febdb 100644 --- a/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.module.css +++ b/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.module.css @@ -1,94 +1,92 @@ .headerContainer { - display: flex; - justify-content: space-between; - margin-top: 31px; - margin-bottom: 24px; - position: relative; + display: flex; + justify-content: space-between; + margin-top: 31px; + margin-bottom: 24px; + position: relative; } .title { - color: var(--Gray-900, #101828); + color: var(--Gray-900, #101828); - /* Display sm/Semibold */ - font-size: 30px; - font-style: normal; - font-weight: 600; - line-height: 38px; /* 126.667% */ + /* Display sm/Semibold */ + font-size: 30px; + font-style: normal; + font-weight: 600; + line-height: 38px; /* 126.667% */ } .warningContainer { - display: flex; - width: 660px; - padding: 4px 10px 4px 4px; - align-items: center; - gap: 8px; - - border-radius: 16px; - background: var(--Warning-50, #FFFAEB); - mix-blend-mode: var(--mix-blend, multiply); + display: flex; + width: 660px; + padding: 4px 10px 4px 4px; + align-items: center; + gap: 8px; + + border-radius: 16px; + background: var(--Warning-50, #fffaeb); + mix-blend-mode: var(--mix-blend, multiply); } - .warningBadge { - display: flex; - padding: 2px 8px; - align-items: center; - - border-radius: 16px; - background: var(--Warning-600, #DC6803); - color: var(--Base-White, #FFF); - text-align: center; - - /* Text xs/Medium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 18px; /* 150% */ + display: flex; + padding: 2px 8px; + align-items: center; + + border-radius: 16px; + background: var(--Warning-600, #dc6803); + color: var(--Base-White, #fff); + text-align: center; + + /* Text xs/Medium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 150% */ } .warningText { - color: var(--Warning-700, #B54708); + color: var(--Warning-700, #b54708); - /* Text xs/Medium */ - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 18px; /* 150% */ + /* Text xs/Medium */ + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 150% */ } .viewContainer { - display: flex; - padding: 0 32px 32px 32px; - flex-direction: column; - align-items: flex-start; - gap: 24px; - flex-grow: 0; - overflow-y: auto; - position: relative; - font-family: Inter, sans-serif; + display: flex; + padding: 0 32px 32px 32px; + flex-direction: column; + align-items: flex-start; + gap: 24px; + flex-grow: 0; + overflow-y: auto; + position: relative; + font-family: Inter, sans-serif; } .warningWrapper { - padding: 0; + padding: 0; } @media (max-width: 1199px) { - .warningWrapper { - padding: 0 16px; - } - - .warningContainer { - width: 100%; - flex-direction: column; - padding: 12px; - align-items: start; - } - - .viewContainer { - padding-left: 0; - padding-right: 0; + .warningWrapper { + padding: 0 16px; + } - } + .warningContainer { + width: 100%; + flex-direction: column; + padding: 12px; + align-items: start; + } + .viewContainer { + display: flex; + padding: 0px 16px 16px 16px; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + } } - - diff --git a/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.tsx b/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.tsx index 69323b27..1e2ac477 100644 --- a/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.tsx +++ b/webapps/world-builder-dashboard/src/pages/FaucetPage/FaucetPage.tsx @@ -1,8 +1,6 @@ // React and hooks import { useEffect, useState } from 'react' import { useQueryClient } from 'react-query' -// Constants -import { L3_NATIVE_TOKEN_SYMBOL } from '../../../constants' // Styles import bridgeStyles from '../BridgePage/BridgePage.module.css' import styles from './FaucetPage.module.css' @@ -44,14 +42,15 @@ const BridgePage = () => {
-
+ {/* TODO: make into component. */} + {/*
Warning
{`This faucet only dispenses ${L3_NATIVE_TOKEN_SYMBOL} tokens. For other tokens, please visit external faucets.`}
-
+
*/}
diff --git a/webapps/world-builder-dashboard/src/utils/bridge/l3Networks.ts b/webapps/world-builder-dashboard/src/utils/bridge/l3Networks.ts index 6129280d..53f0316f 100644 --- a/webapps/world-builder-dashboard/src/utils/bridge/l3Networks.ts +++ b/webapps/world-builder-dashboard/src/utils/bridge/l3Networks.ts @@ -68,7 +68,7 @@ export const L3_NETWORKS = [ utils: '0xDaE6924DAFBefb0eb7Ae2B398958920dE61173F2', validatorWalletCreator: '0x85Feb8fE05794c2384b848235942490a6610C64B', upgradeExecutor: '0xaf0B28462B18df0D7e3b2Ee64684d625f8C3Cb8C', - upgradeExecutorL2: '0xee2439C4C47b84aA718a8f899AECf274Cd759eF6', + upgradeExecutorL2: '0xee2439C4C47b84aA718a8f899AECf274Cd759eF6' }, tokenBridgeContracts: { l2Contracts: { diff --git a/webapps/world-builder-dashboard/src/utils/tokens.ts b/webapps/world-builder-dashboard/src/utils/tokens.ts new file mode 100644 index 00000000..60503283 --- /dev/null +++ b/webapps/world-builder-dashboard/src/utils/tokens.ts @@ -0,0 +1,71 @@ +import { L1_NETWORK, L2_NETWORK, L3_NETWORK } from '../../constants' +import { ZERO_ADDRESS } from './web3utils' +import IconEthereum from '@/assets/IconEthereum' +import IconG7T from '@/assets/IconG7T' +import IconUSDC from '@/assets/IconUSDC' + +export interface Token { + name: string + symbol: string + address: string + Icon: React.FC> + rpc: string +} + +export const getTokensForNetwork = (chainId: number | undefined): Token[] => { + switch (chainId) { + case L1_NETWORK.chainId: + return [ + { + name: 'Game7DAO', + symbol: 'TG7T', + address: L1_NETWORK.g7TokenAddress, + Icon: IconG7T, + rpc: L1_NETWORK.rpcs[0] + }, + { + name: 'USDC', + symbol: 'USDC', + address: '0xf08A50178dfcDe18524640EA6618a1f965821715', // USDC example + Icon: IconUSDC, + rpc: L1_NETWORK.rpcs[0] + }, + { + name: 'Ethereum', + symbol: 'ETH', + address: ZERO_ADDRESS, + Icon: IconEthereum, + rpc: L1_NETWORK.rpcs[0] + } + ] + case L2_NETWORK.chainId: + return [ + { + name: 'Game7DAO', + symbol: 'TG7T', + address: L2_NETWORK.g7TokenAddress, + Icon: IconG7T, + rpc: L2_NETWORK.rpcs[0] + }, + { + name: 'Ethereum', + symbol: 'ETH', + address: ZERO_ADDRESS, + Icon: IconEthereum, + rpc: L2_NETWORK.rpcs[0] + } + ] + case L3_NETWORK.chainId: + return [ + { + name: 'Testnet Game7 Token', + symbol: 'TG7T', + address: L3_NETWORK.g7TokenAddress, + Icon: IconG7T, + rpc: L3_NETWORK.rpcs[0] + } + ] + default: + return [] // Return an empty array or handle unsupported networks + } +} diff --git a/webapps/world-builder-dashboard/src/utils/web3utils.ts b/webapps/world-builder-dashboard/src/utils/web3utils.ts index d314830a..f54bd28a 100644 --- a/webapps/world-builder-dashboard/src/utils/web3utils.ts +++ b/webapps/world-builder-dashboard/src/utils/web3utils.ts @@ -47,4 +47,4 @@ export const formatBigNumber = (bigNumber: ethers.BigNumber, lengthLimit = 25, u const exponent = bigNumberString.length - 1 - units return `${firstDigit}.${remainingDigits}e+${exponent}` -} +} \ No newline at end of file diff --git a/webapps/world-builder-dashboard/vite.config.ts b/webapps/world-builder-dashboard/vite.config.ts index 5fd98ff4..e4692867 100644 --- a/webapps/world-builder-dashboard/vite.config.ts +++ b/webapps/world-builder-dashboard/vite.config.ts @@ -12,5 +12,8 @@ export default defineConfig({ }, css: { postcss: './postcss.config.js' + }, + server: { + port: 3000 } })