Skip to content

Commit

Permalink
feat: ChainSelector component (#182)
Browse files Browse the repository at this point in the history
* feat: ChainSelector component

* feat: hide chains from controls
  • Loading branch information
braianj authored Aug 29, 2024
1 parent aef2d19 commit d69ce8b
Show file tree
Hide file tree
Showing 15 changed files with 937 additions and 34 deletions.
55 changes: 55 additions & 0 deletions src/components/ChainSelector/ChainSelector.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ChainId } from "@dcl/schemas"
import { ChainSelector } from "./ChainSelector"
import { ChainSelectorProps } from "./ChainSelector.types"
import type { Meta, StoryObj } from "@storybook/react"

const meta: Meta = {
component: ChainSelector,
title: "Decentraland UI/Chain Selector",
argTypes: {
chains: {
table: {
disable: true,
},
},
i18n: {
description: "Internationalization",
control: "object",
},
},
render: (args) => (
<ChainSelector
onSelectChain={(chain) => console.log(chain)}
selectedChain={ChainId.ETHEREUM_MAINNET}
chains={[
ChainId.ETHEREUM_MAINNET,
ChainId.MATIC_MAINNET,
ChainId.ARBITRUM_MAINNET,
ChainId.OPTIMISM_MAINNET,
ChainId.BSC_MAINNET,
ChainId.FANTOM_MAINNET,
ChainId.AVALANCHE_MAINNET,
]}
i18n={{
title: "Select Network",
connected: "Connected",
confirmInWallet: "Confirm in wallet",
}}
{...args}
/>
),
}

type Story = StoryObj<ChainSelectorProps>

const Simple: Story = {}
const Confirmed: Story = {
name: "With chain being confirmed",
args: {
chainBeingConfirmed: ChainId.MATIC_MAINNET,
},
}

// eslint-disable-next-line import/no-default-export
export default meta
export { Simple, Confirmed }
187 changes: 187 additions & 0 deletions src/components/ChainSelector/ChainSelector.styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import styled from "@emotion/styled"
import {
Box,
Button,
IconButton,
Paper,
Typography,
useTheme,
} from "@mui/material"
import { neutral, rarity, textOnNeutral } from "../../theme/colors"
import { Modal } from "../Modal/Modal"

const CommonButtonStyle = styled(Button)(() => {
return {
display: "flex",
alignItems: "center",
borderRadius: "14px",
padding: "8px 10px",
color: "green",
"&.MuiButton-sizeMedium.MuiButton-containedSecondary": {
opacity: "1",
"&:not(.Mui-disabled):not(.Mui-focusVisible):not(:hover)": {
color: textOnNeutral.gray1,
background: neutral.gray1,
boxShadow: "none",
},
"&:not(.Mui-disabled):not(.Mui-focusVisible):hover": {
color: textOnNeutral.gray0,
background: neutral.gray0,
},
},
"& span.MuiButton-icon.MuiButton-startIcon.MuiButton-iconSizeMedium": {
"& svg": {
fontSize: "28px",
},
},
}
})

const SelectorButton = styled(CommonButtonStyle)(() => {
return {
"@media (max-width: 991px)": {
"&.MuiButton-sizeMedium.MuiButton-containedSecondary": {
paddingLeft: "0",
paddingRight: "0",
minWidth: "50px",
},
"& span": {
marginLeft: "0",
marginRight: "0",
},
},
}
})

const ChainButton = styled(CommonButtonStyle)((props: {
isSelected: boolean
}) => {
const { isSelected } = props
const theme = useTheme()

let background = "transparent"

if (isSelected) {
background = theme.palette.action.hover
}

return {
alignItems: "flex-start",
justifyContent: "flex-start",
marginBottom: "8px",
"&.MuiButton-sizeMedium.MuiButton-textSecondary:not(.Mui-disabled):not(.Mui-focusVisible):not(:hover)":
{
color: theme.palette.text.primary,
fontSize: "17px",
textTransform: "capitalize" as const,
backgroundColor: background,
},
"&.MuiButton-sizeMedium.MuiButton-textSecondary:not(.Mui-disabled):not(.Mui-focusVisible):hover":
{
color: theme.palette.text.primary,
fontSize: "17px",
textTransform: "capitalize" as const,
},
}
})

const ConnectedLabel = styled(Typography)(() => {
const theme = useTheme()

return {
position: "absolute" as const,
right: "8px",
paddingRight: "16px",
fontSize: "14px",
color: theme.palette.text.primary,
"&:after": {
content: '""',
width: "8px",
height: "8px",
position: "absolute" as const,
right: "0",
top: "calc(50% - 4px)",
borderRadius: "50%",
backgroundColor: rarity.rare,
},
}
})

const ConfirmLabel = styled(ConnectedLabel)({
"&:after": {
backgroundColor: rarity.unique,
},
})

const ChainSelectorModal = styled(Modal)({
"&.MuiModal-root .MuiPaper-root.MuiPaper-elevation.MuiPaper-rounded.MuiPaper-elevation24":
{
width: "100px",
},
" .MuiPaper-root.MuiPaper-elevation.MuiPaper-rounded.MuiPaper-elevation24": {
width: "150px",
},
"& .MuiPaper-root": {
width: "170px",
},
MuiPaper: {
root: {
width: "180px",
},
},
})

const ChainSelectorContainer = styled(Paper)(() => {
return {
position: "absolute" as const,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "360px",
borderRadius: "12px",
"@media (max-width: 700px)": {
minWidth: "100vw",
maxWidth: "100vw",
minHeight: "100vh",
borderRadius: "0px",
margin: "0",
padding: "0",
top: "0",
left: "0",
transform: "translate(0, 0)",
},
}
})

const ChainSelectorWrapper = styled(Box)({
display: "flex",
flexDirection: "column",
margin: "16px",
"@media (max-width: 991px)": {
padding: "5px",
},
})

const ChainSelectorModalTitleContainer = styled(Box)({
display: "flex",
justifyContent: "center",
paddingTop: "24px",
})

const ChainSelectorCloseButton = styled(IconButton)({
marginTop: "-12px",
position: "absolute",
right: 12,
})

export {
SelectorButton,
ChainButton,
ConnectedLabel,
ConfirmLabel,
ChainSelectorModal,
ChainSelectorContainer,
ChainSelectorWrapper,
ChainSelectorModalTitleContainer,
ChainSelectorCloseButton,
}
109 changes: 109 additions & 0 deletions src/components/ChainSelector/ChainSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { ChainId, getChainName } from "@dcl/schemas"
import ClearRoundedIcon from "@mui/icons-material/ClearRounded"
import { Modal, Typography } from "@mui/material"
import { IconChain } from "../IconChain/IconChain"
import { useTabletAndBelowMediaQuery } from "../Media"
import {
ChainNameIconMap,
type ChainSelectorProps,
} from "./ChainSelector.types"
import {
ChainButton,
ChainSelectorCloseButton,
ChainSelectorContainer,
ChainSelectorModalTitleContainer,
ChainSelectorWrapper,
ConfirmLabel,
ConnectedLabel,
SelectorButton,
} from "./ChainSelector.styled"

export const ChainSelector = (props: ChainSelectorProps) => {
const { chains, selectedChain, chainBeingConfirmed, i18n, onSelectChain } =
props

const chainBeingConfirmedRef = useRef(chainBeingConfirmed)
const isMobileOrTablet = useTabletAndBelowMediaQuery()

// This effect is used to close the modal when the chain being confirmed changes
useEffect(() => {
if (selectedChain && selectedChain === chainBeingConfirmedRef.current) {
chainBeingConfirmedRef.current = undefined
setShowModal(false)
}
}, [selectedChain])

const [showModal, setShowModal] = useState(false)

const title = i18n?.title || "Select Network"

const onButtonClick = useCallback(() => {
setShowModal(!showModal)
}, [])

const onSelectChainHandler = useCallback((chainId: ChainId) => {
onSelectChain(chainId)
chainBeingConfirmedRef.current = chainId
}, [])

return (
<>
<SelectorButton
variant="contained"
color="secondary"
startIcon={<IconChain icon={ChainNameIconMap[selectedChain]} />}
onClick={onButtonClick}
>
{!isMobileOrTablet
? selectedChain === ChainId.ETHEREUM_MAINNET
? "Ethereum"
: getChainName(selectedChain)
: null}
</SelectorButton>
<Modal open={showModal} onClose={() => setShowModal(false)}>
<ChainSelectorContainer elevation={1}>
<ChainSelectorModalTitleContainer>
{title && <Typography variant="h5">{title}</Typography>}

<ChainSelectorCloseButton
aria-label="close"
size="large"
onClick={() => setShowModal(false)}
>
<ClearRoundedIcon />
</ChainSelectorCloseButton>
</ChainSelectorModalTitleContainer>

<ChainSelectorWrapper>
{chains.map((chain) => {
const chainName =
chain === ChainId.ETHEREUM_MAINNET
? "Ethereum"
: getChainName(chain)

return (
<ChainButton
key={chain}
variant="text"
color="secondary"
startIcon={<IconChain icon={ChainNameIconMap[chain]} />}
onClick={() => onSelectChainHandler(chain)}
isSelected={selectedChain === chain}
>
{chainName}

{selectedChain === chain ? (
<ConnectedLabel>{i18n.connected}</ConnectedLabel>
) : chainBeingConfirmed && chain === chainBeingConfirmed ? (
<ConfirmLabel>{i18n.confirmInWallet}</ConfirmLabel>
) : null}
</ChainButton>
)
})}
</ChainSelectorWrapper>
</ChainSelectorContainer>
</Modal>
</>
)
}
31 changes: 31 additions & 0 deletions src/components/ChainSelector/ChainSelector.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ChainId } from "@dcl/schemas"

type ChainSelectori18n = {
title: string
connected: React.ReactNode
confirmInWallet: React.ReactNode
}

type ChainSelectorProps = {
selectedChain: ChainId
chainBeingConfirmed?: ChainId
chains: ChainId[]
onSelectChain: (chain: ChainId) => void
i18n: ChainSelectori18n
}

const ChainNameIconMap = {
[ChainId.ETHEREUM_MAINNET]: "ethereum",
[ChainId.ETHEREUM_SEPOLIA]: "ethereum",
[ChainId.MATIC_MAINNET]: "polygon",
[ChainId.MATIC_MUMBAI]: "polygon",
[ChainId.MATIC_AMOY]: "polygon",
[ChainId.ARBITRUM_MAINNET]: "arbitrum",
[ChainId.OPTIMISM_MAINNET]: "optimism",
[ChainId.FANTOM_MAINNET]: "fantom",
[ChainId.BSC_MAINNET]: "bsc",
[ChainId.AVALANCHE_MAINNET]: "avalanche",
}

export { ChainNameIconMap }
export type { ChainSelectori18n, ChainSelectorProps }
Loading

0 comments on commit d69ce8b

Please sign in to comment.