Merge pull request #36 from nimbus-gui/hn.validator-onboarding-responsive

Validator onboarding responsive
This commit is contained in:
Hristo Nedelkov 2024-01-22 20:46:46 +02:00 committed by GitHub
commit 4d145760ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 954 additions and 404 deletions

View File

@ -1,4 +1,4 @@
import { Image } from '@status-im/components'
import { Image } from 'tamagui'
export type IconProps = {
src: string
@ -6,16 +6,8 @@ export type IconProps = {
height?: number
}
const Icon = ({ src, width = 16, height = 16 }: IconProps) => {
return (
<Image
src={src}
source={{ uri: src }}
width={width}
height={height}
style={{ backgroundColor: 'transparent' }}
/>
)
const Icon = ({ src, height = 100, width = 100 }: IconProps) => {
return <Image src={src} source={{ uri: src }} height={height} width={width} />
}
export default Icon

View File

@ -0,0 +1,27 @@
import { CSSProperties, ReactNode } from 'react'
import { XStack, YStack } from 'tamagui'
type ResponsiveStackProps = {
isVerticalAligned?: boolean
children: ReactNode
space?: string
style?: CSSProperties
}
const ResponsiveStack = ({ isVerticalAligned, children, space, style }: ResponsiveStackProps) => {
if (isVerticalAligned) {
return (
<YStack space={space} style={style}>
{children}
</YStack>
)
}
return (
<XStack space={space} style={style}>
{children}
</XStack>
)
}
export default ResponsiveStack

View File

@ -1,5 +1,6 @@
import { Avatar, Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import { CopyIcon } from '@status-im/icons'
import AddCard from '../AddCards/AddCard'
import AlertsList from './AlertsList'
@ -7,9 +8,13 @@ import LogsList from './LogsList'
import DiamondCard from './DiamondCard'
import ValidatorsCount from './ValidatorsCount'
import ValidatorsTabs from './ValidatorsTabs/ValidatorsTabs'
import { getFormattedWalletAddress } from '../../../utilities'
import { copyFunction, getFormattedWalletAddress } from '../../../utilities'
const RightSidebar = () => {
const onCopyWalletAddress = () => {
copyFunction('0xb9dc35')
}
return (
<YStack
width={'320px'}
@ -23,13 +28,21 @@ const RightSidebar = () => {
overflowY: 'auto',
}}
>
<XStack alignItems="center">
<XStack alignItems="center" space={'$2'}>
<Avatar type="user" size={32} name="Ethereum Mainnet" />
<YStack pl="8px">
<YStack>
<Text size={15} weight={'semibold'}>
Ethereum Mainnet
</Text>
<Text size={13}>{getFormattedWalletAddress('0xb9dc35')}</Text>
<XStack space={'$1'} alignItems="center">
<Text size={13}>{getFormattedWalletAddress('0xb9dc35')}</Text>
<CopyIcon
size={16}
color="#647084"
style={{ cursor: 'pointer' }}
onClick={onCopyWalletAddress}
/>
</XStack>
</YStack>
</XStack>
<XStack space={'$2'} alignItems="center" justifyContent="space-between">

View File

@ -1,9 +1,8 @@
import { useState } from 'react'
import { XStack, YStack } from 'tamagui'
import { Avatar, Checkbox, Text } from '@status-im/components'
import { VerifiedIcon, ContactIcon } from '@status-im/icons'
import { XStack } from 'tamagui'
import { Avatar, Checkbox } from '@status-im/components'
import { getFormattedValidatorAddress } from '../../../../utilities'
import ValidatorNameAddress from '../../ValidatorNameAddress'
type ValidatorListItemProps = {
name: string
@ -41,7 +40,7 @@ const ValidatorListItem = ({
}}
width="92%"
>
<XStack alignItems="center">
<XStack alignItems="center" space={'$2'}>
<Avatar
type="user"
size={32}
@ -54,16 +53,12 @@ const ValidatorListItem = ({
[11, 20],
]}
/>
<YStack pl="8px">
<XStack space={'$1'} alignItems="center">
<Text size={13} weight={'semibold'}>
Validator {name}
</Text>
{isVerified && <VerifiedIcon size={20} />}
{isAvatarChipIncluded && <ContactIcon size={20} />}
</XStack>
<Text size={13}>{getFormattedValidatorAddress(validatorAddress)}</Text>
</YStack>
<ValidatorNameAddress
name={name}
address={validatorAddress}
isVerified={isVerified}
isAvatarChipIncluded={isAvatarChipIncluded}
/>
</XStack>
{isSelected && <Checkbox id={name} variant="outline" size={20} selected={isSelected} />}
</XStack>

View File

@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import ValidatorNameAddress from './ValidatorNameAddress'
const meta = {
title: 'General/ValidatorNameAddress',
component: ValidatorNameAddress,
tags: ['autodocs'],
} satisfies Meta<typeof ValidatorNameAddress>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
name: '1',
address: 'zQ3asdf9d4Gs0',
},
}

View File

@ -0,0 +1,63 @@
import { Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import { CopyIcon, VerifiedIcon, ContactIcon, CheckIcon } from '@status-im/icons'
import { useState } from 'react'
import { copyFunction, getFormattedValidatorAddress } from '../../utilities'
type ValidatorNameAddressProps = {
name: string
address: string
isVerified?: boolean
isAvatarChipIncluded?: boolean
}
const ValidatorNameAddress = ({
name,
address,
isVerified,
isAvatarChipIncluded,
}: ValidatorNameAddressProps) => {
const [isCopied, setIsCopied] = useState(false)
const onCopyAddress = () => {
copyFunction(address)
if (isCopied === false) {
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 3000)
}
}
return (
<YStack alignItems={'start'}>
<XStack space={'$1'} alignItems="center">
<Text size={13} weight={'semibold'}>
Validator {name}
</Text>
{isVerified && <VerifiedIcon size={20} />}
{isAvatarChipIncluded && <ContactIcon size={20} />}
</XStack>
<XStack space={'$1'} alignItems="center">
<Text size={13} color="#647084">
{getFormattedValidatorAddress(address)}
</Text>
{isCopied ? (
<CheckIcon size={16} color="#647084" />
) : (
<CopyIcon
size={16}
color="#647084"
style={{ cursor: 'pointer' }}
onClick={onCopyAddress}
/>
)}
</XStack>
</YStack>
)
}
export default ValidatorNameAddress

View File

@ -13,7 +13,7 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
number: 1,
name: '1',
address: 'zQ3asdf9d4Gs0',
},
}

View File

@ -1,31 +1,24 @@
import { Avatar, Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import { Avatar } from '@status-im/components'
import { XStack } from 'tamagui'
import { getFormattedValidatorAddress } from '../../utilities'
import ValidatorNameAddress from './ValidatorNameAddress'
type ValidatorProfileProps = {
number: number
name: string
address: string
}
const ValidatorProfile = ({ number, address }: ValidatorProfileProps) => {
const ValidatorProfile = ({ name, address }: ValidatorProfileProps) => {
return (
<XStack space={'$2'}>
<Avatar
type="user"
size={32}
src="/icons/validator-request.svg"
name={number.toString()}
name={name}
indicator="online"
/>
<YStack>
<Text size={15} weight={'semibold'}>
Validator {number}
</Text>
<Text size={13} color="#647084">
{getFormattedValidatorAddress(address)}
</Text>
</YStack>
<ValidatorNameAddress name={name} address={address} />
</XStack>
)
}

View File

@ -1,10 +1,11 @@
import { useState } from 'react'
import { Input, Text } from '@status-im/components'
import { AddIcon } from '@status-im/icons'
import { Stack, XStack, YStack } from 'tamagui'
import { Stack, XStack, YStack, useMedia } from 'tamagui'
import { CURRENCIES, ETH_PER_VALIDATOR } from '../../constants'
import CurrencyDropdown from './CurrencyDropdown'
import ResponsiveStack from './ResponsiveStack'
type ValidatorsMenuWithPriceProps = {
validatorCount: number
@ -20,6 +21,7 @@ const ValidatorsMenuWithPrice = ({
label,
}: ValidatorsMenuWithPriceProps) => {
const [currency, setCurrency] = useState(Object.keys(CURRENCIES)[0] as CurrencyType)
const media = useMedia()
const changeCurrency = (currency: CurrencyType) => {
if (CURRENCIES[currency]) {
@ -31,7 +33,11 @@ const ValidatorsMenuWithPrice = ({
const totalPrice = totalETH * CURRENCIES[currency as keyof typeof CURRENCIES]
return (
<XStack justifyContent={'space-between'} width={'80%'}>
<ResponsiveStack
isVerticalAligned={media.sm}
style={{ justifyContent: 'space-between', width: media.lg ? '100%' : '80%' }}
space={'$2'}
>
<Stack space={'$2'}>
<Text size={15} weight="regular" color={'#647084'}>
{label}
@ -49,26 +55,30 @@ const ValidatorsMenuWithPrice = ({
onChangeText={changeValidatorCountHandler}
/>
</Stack>
<YStack space={'$2'}>
<Text size={15} weight={'semibold'}>
ETH
</Text>
<Text size={27} weight={'semibold'}>
{totalETH}
</Text>
</YStack>
<YStack space={'$2'}>
<XStack style={{ justifyContent: 'space-between', width: '115%' }}>
<XStack space={'$10'} style={{ justifyContent: 'space-between' }}>
<YStack space={'$2'}>
<Text size={15} weight={'semibold'}>
{currency}
ETH
</Text>
<CurrencyDropdown changeCurrency={changeCurrency} />
</XStack>
<Text size={27} weight={'semibold'}>
{totalPrice.toFixed(2)} {currency}
</Text>
</YStack>
</XStack>
<Stack style={{ marginTop: '2px' }}>
<Text size={27} weight={'semibold'}>
{totalETH}
</Text>
</Stack>
</YStack>
<YStack space={'$2'}>
<XStack style={{ justifyContent: 'space-between' }}>
<Text size={15} weight={'semibold'}>
{currency}
</Text>
<CurrencyDropdown changeCurrency={changeCurrency} />
</XStack>
<Text size={27} weight={'semibold'}>
{totalPrice.toFixed(2)} {currency}
</Text>
</YStack>
</XStack>
</ResponsiveStack>
)
}

View File

@ -2,7 +2,7 @@ import { ReactNode } from 'react'
import { useTheme } from 'tamagui'
import NimbusLogoMark from '../Logos/NimbusLogoMark'
import './layout.css'
import styles from './index.module.css'
type PageWrapperShadowProps = {
breadcrumbBar?: ReactNode
@ -22,19 +22,19 @@ const PageWrapperShadow = ({
const theme = useTheme()
return (
<div className="layout" style={{ backgroundColor: theme.background.val }}>
<section className="layout-left">
<div className={styles['layout']} style={{ backgroundColor: theme.background.val }}>
<section className={styles['layout-left']}>
{breadcrumbBar}
<div className="container">
<div className="container-inner">{children}</div>
<div className={styles['container']}>
<div className={styles['container-inner']}>{children}</div>
</div>
</section>
<section className="layout-right">
<div className="image-container">
<section className={styles['layout-right']}>
<div className={styles['image-container']}>
<img
src={rightImageSrc}
alt="background"
className="background-img"
className={styles['background-img']}
style={{ height: imgHeight }}
/>
{rightImageLogo ? <NimbusLogoMark /> : null}

View File

@ -31,6 +31,49 @@ export const KEYSTORE_FILES = 'KeystoreFiles'
export const RECOVERY_PHRASE = 'Recovery Phrase'
export const BOTH_KEY_AND_RECOVERY = 'Both KeystoreFiles & Recovery Phrase'
export const ETH_PER_VALIDATOR = 32
export const FORM_STEPS = [
{ label: 'Overview', subtitle: 'Get Started' },
{ label: 'Advisories', subtitle: 'Understand your Duties' },
{ label: 'Client Setup', subtitle: 'Execution & Consensus' },
{ label: 'Validator Setup', subtitle: 'Validators & Withdrawal' },
{ label: 'Key Generation', subtitle: 'Secure your Keypairs' },
{ label: 'Deposit', subtitle: 'Stake your ETH' },
{ label: 'Activation', subtitle: 'Complete Setup' },
]
export const ADVISORY_TOPICS: {
[key: string]: string[]
} = {
'Proof of Stake': [
'Proof of Stake systems require validators to hold and lock up a certain amount of cryptocurrency to participate.',
'In Proof of Stake, the chances of creating a block is proportional to the amount of cryptocurrency held.',
'Unlike Proof of Work, Proof of Stake aims to achieve consensus without intensive computational work.',
],
Deposit: [
'Deposits are often irreversible, so ensure to double-check transaction details before confirming.',
'Delay in deposit acknowledgment might be due to network congestion or node synchronization.',
'Always keep transaction IDs or hashes for records and future references in case of disputes.',
],
'Key Management': [
'Storing your private keys on a device connected to the internet is susceptible to hacks and malware.',
'Hardware wallets provide an added layer of security by keeping private keys isolated from online systems.',
'Regularly back up and encrypt your key management solutions to prevent potential losses.',
],
'Bad Behaviour': [
'If you try to cheat the system, or act contrary to the specification, you will be liable to incur a penalty known as slashing.',
'Running your validator keys simultaneously on two or more machines will result in slashing.*',
'Simply being offline with an otherwise healthy network does not result in slashing, but will result in small inactivity penalties.',
],
Requirements: [
'Ensure your system meets the minimum software and hardware requirements before initiating any operations.',
'Staying updated with the latest versions is vital to maintain system integrity and performance.',
'Failure to meet requirements might result in operational inefficiencies or security vulnerabilities.',
],
Risks: [
'Cryptocurrency investments are subject to high volatility and can result in both significant gains and losses.',
'Always do thorough research before making investment decisions or engaging in transactions.',
'Be wary of phishing scams, malicious software, and too-good-to-be-true offers.',
],
}
export const MAC = 'MacOS'
export const WINDOWS = 'Windows'
@ -73,7 +116,7 @@ export const VALIDATOR_TABS_MANAGEMENT = [
export const VALIDATORS_DATA = [
{
number: 1,
name: '1',
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
@ -83,7 +126,7 @@ export const VALIDATORS_DATA = [
status: 'Active',
},
{
number: 1,
name: '1',
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
@ -93,7 +136,7 @@ export const VALIDATORS_DATA = [
status: 'Active',
},
{
number: 1,
name: '1',
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,
@ -103,7 +146,7 @@ export const VALIDATORS_DATA = [
status: 'Active',
},
{
number: 1,
name: '1',
address: 'zQ3asdf9d4Gs0',
balance: 32.0786,
income: 0.0786,

View File

@ -0,0 +1,22 @@
import { useState, useEffect } from 'react'
export const useWindowSize = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
handleResize()
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}

View File

@ -7,6 +7,7 @@ import { Separator, YStack } from 'tamagui'
import { v4 as uuidv4 } from 'uuid'
import styles from './pairDevice.module.css'
import { copyFunction } from '../../utilities'
type GenerateIdProps = {
isAwaitingPairing: boolean
@ -24,7 +25,7 @@ const GenerateId = ({ isAwaitingPairing }: GenerateIdProps) => {
}
const copyGeneratedIdHandler = () => {
navigator.clipboard.writeText(generatedId)
copyFunction(generatedId)
}
return (

View File

@ -15,7 +15,7 @@ type ManagementTableProps = {
}
export type Validator = {
number: number
name: string
address: string
balance: number
income: number
@ -35,12 +35,12 @@ const isValidStatus = (validatorStatus: string, tabStatus: string) => {
return false
}
const isValidNumberOrAddress = (
validatorNumber: number,
const isValidNameOrAddress = (
validatorName: string,
validatorAddress: string,
searchValue: string,
) => {
if (validatorNumber.toString().includes(searchValue) || validatorAddress.includes(searchValue)) {
if (validatorName.includes(searchValue) || validatorAddress.includes(searchValue)) {
return true
}
return false
@ -61,7 +61,7 @@ const ManagementTable = ({ tab, searchValue, changeSearchValue }: ManagementTabl
const filteredValidators = useMemo(() => {
return validators
.filter(validator => isValidStatus(validator.status, tab))
.filter(validator => isValidNumberOrAddress(validator.number, validator.address, searchValue))
.filter(validator => isValidNameOrAddress(validator.name, validator.address, searchValue))
}, [validators, tab, searchValue])
const handleSelectAll = () => {

View File

@ -32,7 +32,7 @@ const ManagementTableRow = ({ validator, isAllSelected }: ManagementTableRowProp
/>
</td>
<td>
<ValidatorProfile number={validator.number} address={validator.address} />
<ValidatorProfile name={validator.name} address={validator.address} />
</td>
<td>
<Text size={15} color={'#647084'} weight={'semibold'}>

View File

@ -6,109 +6,87 @@ import AdvisoriesContent from './AdvisoriesContent'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../redux/store'
import { setSubStepAdvisories } from '../../../redux/ValidatorOnboarding/Advisories/slice'
type AdvisoryTopicsType = {
[key: string]: string[]
}
import { ADVISORY_TOPICS } from '../../../constants'
import styles from './advisoriesLayout.module.css'
const Advisories = () => {
const dispatch = useDispatch()
const { subStepAdvisories } = useSelector((state: RootState) => state.advisories)
const [selectedTitle, setSelectedTitle] = useState(Object.keys(advisoryTopics)[0])
const [completedSteps, setCompletedSteps] = useState<number[]>([])
const [selectedTitle, setSelectedTitle] = useState<string>(Object.keys(ADVISORY_TOPICS)[0])
const unicodeNumbers = ['➀', '➁', '➂', '➃', '➄', '➅']
const advisoriesIcons = unicodeNumbers.map((number, index) =>
index <= subStepAdvisories ? '✓' : number,
)
const isCompleted = (index: number): boolean => completedSteps.includes(index)
const advisoriesIcons = unicodeNumbers.map((number, index) => (isCompleted(index) ? '✓' : number))
useEffect(() => {
setSelectedTitle(Object.keys(advisoryTopics)[subStepAdvisories])
setSelectedTitle(Object.keys(ADVISORY_TOPICS)[subStepAdvisories])
setCompletedSteps(prevSteps => {
if (!prevSteps.includes(subStepAdvisories)) {
return [...prevSteps, subStepAdvisories]
}
return prevSteps
})
}, [subStepAdvisories])
const handleStepClick = (title: string): void => {
const index = getIndexTitle(title)
dispatch(setSubStepAdvisories(index))
}
const isCurrent = (currentTitle: string): boolean => {
const topics = Object.keys(advisoryTopics)
const topics = Object.keys(ADVISORY_TOPICS)
const index = topics.indexOf(currentTitle)
return index <= subStepAdvisories ? true : false
return index === subStepAdvisories
}
const getIndexTitle = (title: string): number => {
const topics = Object.keys(advisoryTopics)
const topics = Object.keys(ADVISORY_TOPICS)
const index = topics.indexOf(title)
return index
}
return (
<XStack
style={{ padding: '30px 33px', justifyContent: 'space-between' }}
minHeight={'65vh'}
width={'100%'}
>
<YStack space={'$2'}>
<div className={styles['advisories-container']}>
<YStack space={'$2'} marginBottom={'30px'}>
<Stack marginBottom="$6">
<Text size={27} weight={'semibold'}>
Advisories
</Text>
</Stack>
{Object.keys(advisoryTopics).map((title, index) => (
<XStack
key={title}
onPress={() => dispatch(setSubStepAdvisories(getIndexTitle(title)))}
style={{ cursor: 'pointer', alignItems: 'center' }}
space={'$2'}
>
<Text
size={19}
weight={isCurrent(title) && 'semibold'}
color={isCurrent(title) ? 'blue' : ''}
<div className={styles['advisories-nav']}>
{Object.keys(ADVISORY_TOPICS).map((title, index) => (
<XStack
key={title}
onPress={() => handleStepClick(title)}
style={{ cursor: 'pointer', alignItems: 'center' }}
space={'$2'}
>
{advisoriesIcons[index]}
</Text>
<Text
size={19}
weight={isCurrent(title) ? 'semibold' : ''}
color={isCurrent(title) ? 'blue' : ''}
>
{title}
</Text>
</XStack>
))}
<Text
size={27}
weight={isCompleted(index) || isCurrent(title) ? 'semibold' : 'normal'}
color={isCompleted(index) || isCurrent(title) ? 'blue' : 'default'}
>
{advisoriesIcons[index]}
</Text>
<Text
size={19}
weight={isCompleted(index) || isCurrent(title) ? 'semibold' : 'normal'}
color={isCompleted(index) || isCurrent(title) ? 'blue' : 'default'}
>
{title}
</Text>
</XStack>
))}
</div>
</YStack>
<AdvisoriesContent title={selectedTitle} content={advisoryTopics[selectedTitle]} />
</XStack>
<AdvisoriesContent title={selectedTitle} content={ADVISORY_TOPICS[selectedTitle]} />
</div>
)
}
export default Advisories
export const advisoryTopics: AdvisoryTopicsType = {
'Proof of Stake': [
'Proof of Stake systems require validators to hold and lock up a certain amount of cryptocurrency to participate.',
'In Proof of Stake, the chances of creating a block is proportional to the amount of cryptocurrency held.',
'Unlike Proof of Work, Proof of Stake aims to achieve consensus without intensive computational work.',
],
Deposit: [
'Deposits are often irreversible, so ensure to double-check transaction details before confirming.',
'Delay in deposit acknowledgment might be due to network congestion or node synchronization.',
'Always keep transaction IDs or hashes for records and future references in case of disputes.',
],
'Key Management': [
'Storing your private keys on a device connected to the internet is susceptible to hacks and malware.',
'Hardware wallets provide an added layer of security by keeping private keys isolated from online systems.',
'Regularly back up and encrypt your key management solutions to prevent potential losses.',
],
'Bad Behaviour': [
'If you try to cheat the system, or act contrary to the specification, you will be liable to incur a penalty known as slashing.',
'Running your validator keys simultaneously on two or more machines will result in slashing.*',
'Simply being offline with an otherwise healthy network does not result in slashing, but will result in small inactivity penalties.',
],
Requirements: [
'Ensure your system meets the minimum software and hardware requirements before initiating any operations.',
'Staying updated with the latest versions is vital to maintain system integrity and performance.',
'Failure to meet requirements might result in operational inefficiencies or security vulnerabilities.',
],
Risks: [
'Cryptocurrency investments are subject to high volatility and can result in both significant gains and losses.',
'Always do thorough research before making investment decisions or engaging in transactions.',
'Be wary of phishing scams, malicious software, and too-good-to-be-true offers.',
],
}

View File

@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-react-router-v6'
import AdvisoriesContent from './AdvisoriesContent'
import { advisoryTopics } from './Advisories'
import { ADVISORY_TOPICS } from '../../../constants'
const meta = {
title: 'ValidatorOnboarding/AdvisoriesContent',
@ -17,8 +17,8 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
const advisoryTopicsKeys = Object.keys(advisoryTopics)
const advisoryTopicsValues = Object.values(advisoryTopics)
const advisoryTopicsKeys = Object.keys(ADVISORY_TOPICS)
const advisoryTopicsValues = Object.values(ADVISORY_TOPICS)
export const ProofOfStake: Story = {
args: {

View File

@ -0,0 +1,25 @@
.advisories-container {
display: flex;
padding: 30px;
justify-content: space-between;
width: auto;
}
.advisories-nav {
display: flex;
flex-direction: column;
gap: 5px;
}
@media screen and (max-width: 780px) {
.advisories-container {
flex-direction: column;
align-items: start;
gap: 20px;
}
.advisories-nav {
width: 100%;
gap: 10px;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
}
}

View File

@ -15,7 +15,6 @@ import {
setIsCopyPastedPhrase,
setValidWords,
} from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
import { setSubStepAdvisories } from '../../redux/ValidatorOnboarding/Advisories/slice'
const ContinueButton = () => {
const [isDisabled, setIsDisabled] = useState(false)
@ -32,7 +31,6 @@ const ContinueButton = () => {
(state: RootState) => state.validatorOnboarding,
)
const { isValidatorSet } = useSelector((state: RootState) => state.validatorSetup)
const { subStepAdvisories } = useSelector((state: RootState) => state.advisories)
const dispatch = useDispatch()
const navigate = useNavigate()
@ -64,12 +62,7 @@ const ContinueButton = () => {
])
const handleStep1 = () => {
if (subStepAdvisories < 5) {
dispatch(setSubStepAdvisories(subStepAdvisories + 1))
} else {
dispatch(setSubStepAdvisories(0))
dispatch(setActiveStep(activeStep + 1))
}
dispatch(setActiveStep(activeStep + 1))
}
const handleStep2 = () => {
@ -124,7 +117,7 @@ const ContinueButton = () => {
justifyContent: isActivationValScreen ? 'space-between' : 'end',
alignItems: 'center',
zIndex: 1000,
marginTop: '10px',
marginTop: '21px',
}}
>
{isCopyPastedPhrase && (

View File

@ -52,7 +52,7 @@ const Deposit = () => {
{Array.from({ length: validatorCount }).map((_, index) => (
<ValidatorRequest
key={index}
number={index + 1}
name={(index + 1).toString()}
isTransactionConfirmation={isTransactionConfirmation}
/>
))}

View File

@ -16,21 +16,21 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
number: 1,
name: '1',
isTransactionConfirmation: false,
},
}
export const TransactionConfirmation: Story = {
args: {
number: 1,
name: '1',
isTransactionConfirmation: true,
},
}
export const BigNumber: Story = {
args: {
number: 123456789,
name: '123456789',
isTransactionConfirmation: false,
},
}

View File

@ -5,11 +5,11 @@ import TransactionStatus from './TransactionStatus'
import ValidatorProfile from '../../../../components/General/ValidatorProfile'
type ValidatorRequestProps = {
number: number
name: string
isTransactionConfirmation?: boolean
}
const ValidatorRequest = ({ number, isTransactionConfirmation }: ValidatorRequestProps) => {
const ValidatorRequest = ({ name, isTransactionConfirmation }: ValidatorRequestProps) => {
let transactionStatus = 'Complete'
const isTransactionCompleted = transactionStatus === 'Complete'
@ -17,7 +17,7 @@ const ValidatorRequest = ({ number, isTransactionConfirmation }: ValidatorReques
<YStack space={'$3'} style={{ width: '100%' }}>
<XStack style={{ justifyContent: 'space-between', width: '100%', alignItems: 'center' }}>
<XStack style={{ justifyContent: 'space-between', width: '44%', alignItems: 'center' }}>
<ValidatorProfile number={number} address={'zQ3asdf9d4Gs0'} />
<ValidatorProfile name={name} address={'zQ3asdf9d4Gs0'} />
<Text size={13} color="#647084" weight={'semibold'}>
Keys Generated
</Text>

View File

@ -53,9 +53,15 @@ span[class*='Connector-'] {
content: attr(data-subtitle);
position: absolute;
top: calc(100% + 4px);
left: 8px;
left: 9px;
font-size: 12px;
font-family: 'Inter', sans-serif;
color: #a2a9b0;
width: max-content;
}
@media (max-width: 410px) {
.custom-step::after {
font-size: 10px;
}
}

View File

@ -2,24 +2,52 @@ import { Stepper, Step } from 'react-form-stepper'
import { useDispatch } from 'react-redux'
import { setActiveStep } from '../../../redux/ValidatorOnboarding/slice'
import { FORM_STEPS } from '../../../constants'
import { useWindowSize } from '../../../hooks/useWindowSize'
import './FormStepper.css'
const steps = [
{ label: 'Overview', subtitle: 'Get Started' },
{ label: 'Advisories', subtitle: 'Understand your Duties' },
{ label: 'Client Setup', subtitle: 'Execution & Consensus' },
{ label: 'Validator Setup', subtitle: 'Validators & Withdrawal' },
{ label: 'Key Generation', subtitle: 'Secure your Keypairs' },
{ label: 'Deposit', subtitle: 'Stake your ETH' },
{ label: 'Activation', subtitle: 'Complete Setup' },
]
type FormStepperProps = {
activeStep: number
}
const FormStepper = ({ activeStep }: FormStepperProps) => {
const dispatch = useDispatch()
const windowSize = useWindowSize()
const getIsStepVisible = (index: number, stepsBefore: number, stepsAfter: number) => {
const totalSteps = FORM_STEPS.length
let start = activeStep - stepsBefore
let end = activeStep + stepsAfter
// active step is near the start or end
if (start < 0) {
end -= start
start = 0
}
if (end >= totalSteps) {
start -= end - totalSteps + 1
end = totalSteps - 1
}
start = Math.max(0, start)
end = Math.min(end, totalSteps - 1)
return index >= start && index <= end
}
const isStepVisible = (index: number) => {
if (windowSize.width < 774) {
return getIsStepVisible(index, 1, 1) // 3 steps (1 before, 1 after)
} else if (windowSize.width < 963) {
return getIsStepVisible(index, 1, 2) // 4 steps
} else if (windowSize.width < 1183) {
return getIsStepVisible(index, 1, 3) // 5 steps
} else if (windowSize.width < 1300) {
return getIsStepVisible(index, 2, 3) // 6 steps
} else {
return true
}
}
const changeStepOnClickHandler = (index: number) => {
if (activeStep > index) {
@ -37,21 +65,25 @@ const FormStepper = ({ activeStep }: FormStepperProps) => {
fontSize: '14px',
zIndex: 1,
width: '100%',
height: 'fit-content',
padding: 0,
marginBottom: '3rem',
}}
>
{steps.map((step, index) => (
<Step
key={index}
label={step.label}
className="custom-step"
onClick={() => changeStepOnClickHandler(index)}
completed={activeStep > index - 1}
data-subtitle={step.subtitle}
data-step={step.label}
/>
))}
{FORM_STEPS.filter((_, index) => isStepVisible(index)).map(step => {
const originalIndex = FORM_STEPS.indexOf(step)
return (
<Step
key={originalIndex}
label={`${step.label}`}
className="custom-step"
onClick={() => changeStepOnClickHandler(originalIndex)}
completed={activeStep > originalIndex - 1}
data-subtitle={step.subtitle}
data-step={step.label}
/>
)
})}
</Stepper>
)
}
@ -69,8 +101,8 @@ const stepStyle = {
activeTextColor: '#ffffff',
completedTextColor: '#ffffff',
inactiveTextColor: '#000000',
size: '20px',
circleFontSize: '10px',
size: '28px',
circleFontSize: '0px',
labelFontSize: '14px',
borderRadius: '50%',
fontWeight: 700,

View File

@ -1,10 +1,11 @@
import { Stack, YStack } from 'tamagui'
import { YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useSelector } from 'react-redux'
import AutocompleteInput from './AutocompleteInput'
import KeyGenerationTitle from '../KeyGenerationTitle'
import { useSelector } from 'react-redux'
import { RootState } from '../../../../redux/store'
import styles from '../index.module.css'
const ConfirmRecoveryPhrase = () => {
const { validWords } = useSelector((state: RootState) => state.keyGeneration)
@ -13,19 +14,11 @@ const ConfirmRecoveryPhrase = () => {
<YStack space={'$3'} style={{ width: '100%', marginTop: '20px' }}>
<KeyGenerationTitle />
<Text size={19}>Confirm Recovery Phrase</Text>
<Stack
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '20px 40px',
width: '72%',
marginBottom: '10px',
}}
>
<div className={styles['confirm-recovery-phrase']}>
{validWords.map((_, index) => (
<AutocompleteInput key={index} index={index} />
))}
</Stack>
</div>
</YStack>
)
}

View File

@ -1,6 +1,9 @@
import { useEffect } from 'react'
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useSelector } from 'react-redux'
import { generateMnemonic } from 'web-bip39'
import { useDispatch, useSelector } from 'react-redux'
import wordlist from 'web-bip39/wordlists/english'
import KeyGenerationHeader from './KeyGenerationHeader/KeyGenerationHeader'
import RecoveryMechanism from './RecoveryMechanism/RecoveryMechanism'
@ -9,11 +12,22 @@ import RecoveryPhrase from './RecoveryPhrase'
import ConfirmRecoveryPhrase from './ConfirmRecoveryPhrase/ConfirmRecoveryPhrase'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../constants'
import { RootState } from '../../../redux/store'
import { setGeneratedMnemonic } from '../../../redux/ValidatorOnboarding/KeyGeneration/slice'
const KeyGeneration = () => {
const { recoveryMechanism, isConfirmPhraseStage } = useSelector(
(state: RootState) => state.keyGeneration,
)
const dispatch = useDispatch()
useEffect(() => {
getMnemonic()
}, [])
const getMnemonic = async () => {
const mnemonic = await generateMnemonic(wordlist, 256)
dispatch(setGeneratedMnemonic(mnemonic.split(' ')))
}
const isKeystoreFiles =
recoveryMechanism === KEYSTORE_FILES || recoveryMechanism === BOTH_KEY_AND_RECOVERY

View File

@ -1,13 +1,12 @@
import { XStack } from 'tamagui'
import SyncStatusCard from '../../../../components/General/SyncStatusCard'
import KeyGenerationTitle from '../KeyGenerationTitle'
import styles from '../index.module.css'
const KeyGenerationHeader = () => {
return (
<XStack style={{ width: '100%', justifyContent: 'space-between' }}>
<div className={styles['header']}>
<KeyGenerationTitle />
<XStack space={'$2'}>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<SyncStatusCard
synced={123.524}
total={172.503}
@ -20,8 +19,8 @@ const KeyGenerationHeader = () => {
title="Consensus Sync Status"
color="#ff6161"
/>
</XStack>
</XStack>
</div>
</div>
)
}

View File

@ -16,9 +16,7 @@ const KeystoreBackupsCard = () => {
style={{
border: '1px solid #DCE0E5',
borderRadius: '16px',
padding: '12px 16px',
width: '32%',
marginTop: '3.4%',
padding: '9px 16px',
cursor: 'pointer',
}}
onClick={downloadKeyFilesHandler}
@ -31,7 +29,6 @@ const KeystoreBackupsCard = () => {
justifyContent: 'space-between',
width: '100%',
alignItems: 'center',
marginTop: '8px',
}}
>
<Text size={13} color="#647084">

View File

@ -1,9 +1,10 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Stack, YStack, useMedia } from 'tamagui'
import { Button, InformationBox, Input, Text } from '@status-im/components'
import { ClearIcon, CloseCircleIcon } from '@status-im/icons'
import { useState } from 'react'
import KeystoreBackupsCard from './KeystoreBackupsCard'
import ResponsiveStack from '../../../../components/General/ResponsiveStack'
const KeystoreFiles = () => {
const [encryptedPassword, setEncryptedPassword] = useState('')
@ -12,6 +13,7 @@ const KeystoreFiles = () => {
const [confirmEncryptedPasswordError, setConfirmEncryptedPasswordError] = useState(false)
const [displayEncryptedPassword, setDisplayEncryptedPassword] = useState('')
const [displayConfirmEncryptedPassword, setDisplayConfirmEncryptedPassword] = useState('')
const media = useMedia()
const generateKeystoreFilesHandler = () => {
if (
@ -55,8 +57,12 @@ const KeystoreFiles = () => {
return (
<YStack space={'$4'}>
<XStack space={'$2'} style={{ justifyContent: 'space-between', width: '100%' }}>
<YStack space={'$4'} style={{ width: '66%' }}>
<ResponsiveStack
isVerticalAligned={!!media.sm}
space={'$2'}
style={{ justifyContent: 'space-between', width: '100%' }}
>
<YStack space={'$4'} style={{ width: media.sm ? '100%' : '66%' }}>
<YStack space={'$4'}>
<Text size={15} color={'#647084'}>
Encryption Password
@ -96,8 +102,10 @@ const KeystoreFiles = () => {
/>
</YStack>
</YStack>
<KeystoreBackupsCard />
</XStack>
<div style={{ width: media.sm ? '100%' : '32%', paddingTop: '3.8%' }}>
<KeystoreBackupsCard />
</div>
</ResponsiveStack>
<Stack style={{ width: 'fit-content' }}>
<Button onPress={generateKeystoreFilesHandler}>Generate Key files</Button>
</Stack>

View File

@ -1,8 +1,9 @@
import { Text } from '@status-im/components'
import { XStack, YStack } from 'tamagui'
import { YStack } from 'tamagui'
import RecoveryMechanismCard from './RecoveryMechanismCard'
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
import styles from '../index.module.css'
type RecoveryMechanismProps = {
recoveryMechanism: string
@ -20,7 +21,7 @@ const RecoveryMechanism = ({ recoveryMechanism }: RecoveryMechanismProps) => {
<Text size={19} weight={'semibold'}>
Select Recovery Mechanism
</Text>
<XStack space={'$4'} style={{ justifyContent: 'space-between', marginTop: '40px' }}>
<div className={styles['recovery-mechanism-container']}>
{Object.entries(cards).map(([value, icon]) => (
<RecoveryMechanismCard
key={value}
@ -29,7 +30,7 @@ const RecoveryMechanism = ({ recoveryMechanism }: RecoveryMechanismProps) => {
icon={icon}
/>
))}
</XStack>
</div>
</YStack>
)
}

View File

@ -2,12 +2,11 @@ import { Stack, XStack, YStack } from 'tamagui'
import { Button, InformationBox, Text } from '@status-im/components'
import { CloseCircleIcon, CopyIcon, CheckIcon } from '@status-im/icons'
import { useEffect, useState } from 'react'
import { generateMnemonic } from 'web-bip39'
import { useDispatch, useSelector } from 'react-redux'
import wordlist from 'web-bip39/wordlists/english'
import { useSelector } from 'react-redux'
import { RootState } from '../../../redux/store'
import { setGeneratedMnemonic } from '../../../redux/ValidatorOnboarding/KeyGeneration/slice'
import { copyFunction } from '../../../utilities'
import styles from './index.module.css'
type RecoveryPhraseProps = {
isKeystoreFiles: boolean
@ -17,26 +16,25 @@ const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
const [isReveal, setIsReveal] = useState(false)
const [isCopied, setIsCopied] = useState(false)
const { generatedMnemonic } = useSelector((state: RootState) => state.keyGeneration)
const dispatch = useDispatch()
useEffect(() => {
getMnemonic()
}, [])
const getMnemonic = async () => {
const mnemonic = await generateMnemonic(wordlist, 256)
dispatch(setGeneratedMnemonic(mnemonic.split(' ')))
}
setIsCopied(false)
}, [generatedMnemonic])
const revealHandler = () => {
setIsReveal(state => !state)
}
const copyRecoveryPhraseHandler = () => {
const text = generatedMnemonic.join(' ')
navigator.clipboard.writeText(text)
copyFunction(generatedMnemonic.join(' '))
setIsCopied(true)
if (isCopied === false) {
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 3000)
}
}
return (
@ -51,24 +49,21 @@ const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
cursor: 'pointer',
paddingBottom: '8px',
paddingRight: '18px',
paddingLeft: '18px',
}}
onClick={copyRecoveryPhraseHandler}
>
<Stack
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(6, 1fr)',
gap: '5px 3px',
width: '100%',
filter: `blur(${isReveal ? '0px' : '4px'})`,
padding: '28px 0px 0px 18px',
}}
className={styles['recovery-phrase']}
>
{generatedMnemonic.map((word, index) => (
<XStack style={{ width: '100%' }}>
<Stack style={{ width: '25%' }}>
<XStack style={{ width: '100%' }} key={word}>
<Stack>
<Text key={index} size={19} weight={'semibold'} color="#0d162566">
{index + 1}.
{index + 1}.&nbsp;
</Text>
</Stack>
<Text key={index} size={19} weight={'semibold'}>
@ -76,7 +71,7 @@ const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
</Text>
</XStack>
))}
</Stack>
</div>
{isCopied ? <CheckIcon size={20} /> : <CopyIcon size={20} />}
</YStack>
<Stack style={{ width: 'fit-content', marginBottom: '12px' }}>

View File

@ -0,0 +1,99 @@
.recovery-mechanism-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: space-between;
margin-top: 40px;
}
.recovery-mechanism-container > div {
flex: 1;
width: 30%;
}
.header {
width: 100%;
justify-content: space-between;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.recovery-phrase {
display: grid;
gap: 5px 3px;
width: 100%;
padding: 28px 0px 0px 18px;
grid-template-columns: repeat(6, 1fr);
}
.confirm-recovery-phrase {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px 40px;
width: 72%;
margin-bottom: 10px;
}
@media (max-width: 630px) {
.recovery-mechanism-container > div {
min-width: 40%;
}
}
@media (max-width: 740px) {
.header {
margin-bottom: 16px;
}
}
@media (min-width: 701px) and (max-width: 950px) {
.recovery-phrase {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 700px) {
.recovery-phrase {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 351px) and (max-width: 520px) {
.recovery-phrase {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 350px) {
.recovery-phrase {
grid-template-columns: repeat(1, 1fr);
}
}
@media (max-width: 900px) {
.confirm-recovery-phrase {
width: 100%;
}
}
@media (max-width: 750px) {
.confirm-recovery-phrase {
grid-template-columns: repeat(3, 1fr);
width: 100%;
}
}
@media (max-width: 550px) {
.confirm-recovery-phrase {
grid-template-columns: repeat(2, 1fr);
width: 100%;
}
}
@media (max-width: 350px) {
.confirm-recovery-phrase {
grid-template-columns: repeat(1, 1fr);
width: 100%;
}
}

View File

@ -1,60 +1,48 @@
import { XStack, YStack } from 'tamagui'
import { YStack } from 'tamagui'
import { Text } from '@status-im/components'
import OverviewCard from './OverviewCard'
import LinkWithArrow from '../../../components/General/LinkWithArrow'
import OverviewWrapper from './OverviewWrapper'
import styles from './overviewLayout.module.css'
const Overview = () => {
return (
<>
<YStack
className="layout-left"
space={'$5'}
style={{ padding: '26px 0 32px 32px' }}
minHeight={'65vh'}
justifyContent={'space-between'}
>
<YStack space={'$5'}>
<Text size={27} weight={'semibold'}>
Overview
</Text>
<Text size={19}>
Becoming a validator is a big responsibility with important preparation steps. Only
start the deposit process when you're ready.
</Text>
<Text size={15} color="#939BA1">
By running a validator, you'll be responsible for securing the network and receive
continuous payouts for actions that help the network reach consensus.
</Text>
<Text size={15} color="#939BA1">
Since the successful transition to proof-of-stake via The Merge, Ethereum is fully
secured by proof-of-stake validators. By running a validator, you'll be helping to
secure the Ethereum network.
</Text>
<LinkWithArrow
text="Learn More"
to={'/'}
arrowRight={true}
style={{ marginBottom: '1%', fontSize: '13px' }}
/>
</YStack>
<XStack space={'$3'}>
<OverviewCard text={'Current APR'} value={'4.40%'} />
<OverviewCard text={'Total ETH Staked'} value={'9,451,123'} />
<OverviewCard text={'Estimated Activation Time'} value={'32 Days'} />
<OverviewCard text={'Validator Queue'} value={'92603'} />
</XStack>
<OverviewWrapper
imgHeight="250%"
rightImageSrc="./background-images/sync-status-background.png"
>
<YStack space={'$5'} marginTop={'2rem'} width="100%">
<Text size={27} weight={'semibold'}>
Overview
</Text>
<Text size={19}>
Becoming a validator is a big responsibility with important preparation steps. Only start
the deposit process when you're ready.
</Text>
<Text size={15} color="#939BA1">
By running a validator, you'll be responsible for securing the network and receive
continuous payouts for actions that help the network reach consensus.
</Text>
<Text size={15} color="#939BA1">
Since the successful transition to proof-of-stake via The Merge, Ethereum is fully secured
by proof-of-stake validators. By running a validator, you'll be helping to secure the
Ethereum network.
</Text>
<LinkWithArrow
text="Learn More"
to={'/'}
arrowRight={true}
style={{ marginBottom: '1%', fontSize: '13px' }}
/>
</YStack>
<section className="layout-right">
<div className="image-container">
<img
src="./background-images/sync-status-background.png"
alt="background"
className="background-img"
/>
</div>
</section>
</>
<div className={styles.overviewCards}>
<OverviewCard text={'Current APR'} value={'4.40%'} />
<OverviewCard text={'Total ETH Staked'} value={'9,451,123'} />
<OverviewCard text={'Estimated Activation Time'} value={'32 Days'} />
<OverviewCard text={'Validator Queue'} value={'92603'} />
</div>
</OverviewWrapper>
)
}

View File

@ -1,6 +1,5 @@
import { YStack } from 'tamagui'
import { Text } from '@status-im/components'
import styles from './overviewLayout.module.css'
import { Text, YStack } from 'tamagui'
type OverviewCardProps = {
text: string
value: string
@ -8,21 +7,22 @@ type OverviewCardProps = {
const OverviewCard = ({ text, value }: OverviewCardProps) => {
return (
<YStack
style={{
borderRadius: '16px',
border: '1px solid rgba(0, 0, 0, 0.15)',
width: '44%',
padding: '12px 16px',
backgroundColor: '#FFF',
}}
>
<Text size={15} weight={'semibold'}>
{text}
</Text>
<Text size={27} color="blue" weight={'semibold'}>
{value}
</Text>
<YStack>
<div className={styles.overviewCard}>
<Text
fontWeight={'500'}
style={{ display: 'block', marginBottom: '8px', fontSize: '15px' }}
>
{text}
</Text>
<Text
color="blue"
fontWeight={'500'}
style={{ display: 'block', marginBottom: '8px', fontSize: '27px' }}
>
{value}
</Text>
</div>
</YStack>
)
}

View File

@ -0,0 +1,36 @@
import { ReactNode } from 'react'
import { useTheme } from 'tamagui'
import styles from './validatorLayout.module.css'
type OverviewWrapperProps = {
rightImageSrc?: string
children: ReactNode
imgHeight?: string
}
const OverviewWrapper = ({ rightImageSrc, children, imgHeight }: OverviewWrapperProps) => {
const theme = useTheme()
return (
<div className={styles['layout']} style={{ backgroundColor: theme.background.val }}>
<section className={styles['layout-left']}>
<div className={styles['container']}>
<div className={styles['container-inner']}>{children}</div>
</div>
</section>
<section className={styles['layout-right']}>
<div className={styles['image-container']}>
<img
src={rightImageSrc}
alt="background"
className={styles['background-img']}
style={{ height: imgHeight }}
/>
</div>
</section>
</div>
)
}
export default OverviewWrapper

View File

@ -0,0 +1,24 @@
.overviewCards {
display: flex;
flex-direction: row;
gap: 16px;
flex-wrap: wrap;
width: 250%;
}
.overviewCard {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.15);
padding: 12px 16px;
background-color: #fff;
min-width: 180px;
}
@media screen and (max-width: 1000px) {
.overviewCards {
flex-direction: column;
}
.overviewCard {
width: 35%;
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,166 @@
.layout {
background-color: #fff;
height: 100%;
position: relative;
display: flex;
flex-wrap: wrap;
overflow: hidden;
border-radius: 25px;
}
.layout::after {
display: block;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layout-left {
flex: 0 0 55%;
max-width: 55%;
z-index: 2;
}
.container {
display: flex;
flex-wrap: wrap;
height: 100%;
padding-left: 10%;
}
.container-inner {
max-width: 70%;
flex: 1 0 70%;
display: flex;
flex-direction: column;
}
.content {
flex-grow: 1;
}
/* LAYOUT RIGHT ELEMENT WITH IMAGE TAKING UP THE WHOLE HIGHT OF THE VIEWPORT */
.layout-right {
flex: 0 0 45%;
max-width: 45%;
z-index: 0;
}
.image-container {
height: 100%;
position: relative;
overflow: hidden;
color: #fff;
}
.image-container::before {
display: block;
content: '';
padding-bottom: 100%;
}
.image-container::after {
display: block;
content: '';
position: absolute;
top: 0;
left: -1%;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 1) 20%, rgba(255, 255, 255, 0));
}
.image-container .background-img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 140%;
width: auto;
}
.image-container .nimbus-logomark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.image-container .nimbus-logomark svg {
height: 73px;
}
@media (max-width: 1000px) {
.layout {
height: auto;
}
.layout-left {
flex: 0 0 100%;
max-width: 100%;
order: 1;
}
.container {
justify-content: start;
padding: 20px;
}
.container-inner {
max-width: 100%;
flex: 1 0 100%;
}
.layout-right {
flex: 0 0 100%;
max-width: 100%;
order: 0;
margin-top: -10%;
margin-bottom: -72%;
}
.image-container {
margin: 0;
height: auto;
position: relative;
overflow: hidden;
}
.image-container .background-img {
width: 100%;
position: absolute;
top: 10%;
left: 50%;
/* transform: translateX(-50%) translateY(-5%); */
clip-path: inset(0 0 85% 0);
height: auto;
}
.image-container .nimbus-logomark {
display: none;
}
.content,
.breadcrumbBar,
.other-elements {
margin: 0;
padding: 0;
}
.breadcrumbBar,
.some-other-element {
margin: 0;
padding: 0;
}
.image-container {
position: relative;
overflow: hidden;
padding: 0;
margin: 0;
}
.image-container .background-img {
clip-path: polygon(0 0, 100% 0, 100% 40%, 0 40%);
}
.image-container::after {
width: 100%;
right: 0;
left: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 1) 62%, rgba(255, 255, 255, 0));
}
}

View File

@ -16,6 +16,7 @@ const ValidatorBoxWrapper = ({ children }: ValidatorBoxWrapperProps) => {
backgroundColor: '#fff',
zIndex: 1,
width: '100%',
minHeight: '60vh',
}}
>
{children}

View File

@ -1,5 +1,4 @@
import { YStack } from 'tamagui'
import { useSelector } from 'react-redux'
import { RootState } from '../../redux/store'
@ -15,26 +14,23 @@ import Advisories from './Advisories/Advisories'
import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup'
import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall'
import ContinueButton from './ContinueButton'
import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup'
import './layoutGradient.css'
import Deposit from './Deposit/Deposit'
import './layoutGradient.css'
import { useWindowSize } from '../../hooks/useWindowSize'
import styles from './layoutGradient.module.css'
const ValidatorOnboarding = () => {
const { activeStep, subStepValidatorSetup } = useSelector(
(state: RootState) => state.validatorOnboarding,
)
const windowSize = useWindowSize()
return (
<div className="gradient-wrapper">
<div className={styles.gradientWrapper}>
<YStack
style={{
width: '100%',
maxWidth: '1100px',
margin: '4rem auto 2rem',
padding: windowSize.width < 1000 ? '5% 2% 3% 2%' : '3% 13% 1% 13%',
justifyContent: 'start',
alignItems: 'start',
}}
@ -48,13 +44,11 @@ const ValidatorOnboarding = () => {
<ValidatorBoxWrapper>
{activeStep === 0 && <Overview />}
{activeStep === 1 && <Advisories />}
{activeStep === 2 && subStepValidatorSetup === 0 && <ValidatorSetup />}
{activeStep === 2 && subStepValidatorSetup === 1 && <ValidatorSetupInstall />}
{activeStep === 2 && subStepValidatorSetup === 2 && <ConsensusSelection />}
{activeStep === 2 && subStepValidatorSetup === 3 && <ActivationValidatorSetup />}
{activeStep === 3 && <ClientSetup />}
{activeStep === 4 && <KeyGeneration />}
{activeStep === 5 && <Deposit />}
{activeStep === 6 && (

View File

@ -33,12 +33,13 @@ const ConsensusSelection = () => {
return (
<YStack style={{ width: '100%', padding: '32px' }} minHeight={'65vh'}>
<XStack justifyContent={'space-between'} alignItems={'center'} mb={'30px'}>
<XStack justifyContent={'space-between'} alignItems={'center'} mb={'30px'} flexWrap="wrap">
<Text size={27} weight={'semibold'}>
Validator Setup
</Text>
<XStack space={'$2'}>
<XStack space={'$2'} flexWrap="wrap">
<PairedDeviceCard />
<ConsensusGaugeCard
color="blue"
synced={134879}
@ -67,9 +68,9 @@ const ConsensusSelection = () => {
Install Consensus client
</TextTam>
<XStack space={'$8'}>
<XStack space={'$8'} flexWrap="wrap">
<ConsensusClientCard name={clients[0].name} icon={clients[0].icon} />
<YStack width={'67%'} space={'$4'}>
<YStack width={'67%'} maxWidth="550px" space={'$4'}>
<Text size={19}>The resource efficient Ethereum Clients.</Text>
<Text size={15}>
{selectedClient} is a client implementation for both execution and consensus layers that

View File

@ -0,0 +1,42 @@
.osCardsContainer {
display: flex;
justify-content: space-between;
display: grid;
grid-gap: 15px;
grid-template-columns: repeat(3, 1fr);
}
.osCard {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 16px;
padding: 12px 16px;
cursor: pointer;
box-sizing: border-box;
}
.osCardSelected {
background-color: #2a4af50d;
border: 1px solid #2a4af566;
}
@media (max-width: 1000px) {
.osCardsContainer {
grid-template-columns: repeat(2, 1fr);
}
.osCard:nth-child(3) {
width: 205%;
}
}
@media (max-width: 750px) {
.osCardsContainer {
grid-template-columns: repeat(1, 1fr);
}
.osCard {
width: 100%;
}
.osCard:nth-child(3) {
width: 100%;
}
}

View File

@ -16,7 +16,6 @@ export const MacOS: Story = {
args: {
icon: '/icons/apple-logo.svg',
name: MAC,
isSelected: true,
},
}
@ -24,7 +23,6 @@ export const Linux: Story = {
args: {
icon: '/icons/linux-logo.svg',
name: LINUX,
isSelected: true,
},
}
@ -32,7 +30,6 @@ export const Windows: Story = {
args: {
icon: '/icons/windows-logo.svg',
name: WINDOWS,
isSelected: true,
},
}
@ -40,6 +37,5 @@ export const NotSelectedMacOS = {
args: {
icon: '/icons/apple-logo.svg',
name: MAC,
isSelected: false,
},
}

View File

@ -1,36 +1,19 @@
import { Stack, YStack } from 'tamagui'
import { Stack } from 'tamagui'
import { Text } from '@status-im/components'
import Icon from '../../../../components/General/Icon'
type OSCardProps = {
name: string
icon: string
onClick?: () => void
isSelected?: boolean
}
const OSCard = ({ name, icon, onClick, isSelected }: OSCardProps) => {
const OSCard = ({ name, icon }: OSCardProps) => {
return (
<YStack
style={{
backgroundColor: isSelected ? '#2A4AF50D' : 'none',
border: isSelected ? '1px solid #2A4AF566' : '1px solid rgba(0, 0, 0, 0.15);',
borderRadius: '16px',
padding: '12px 16px',
width: '32%',
cursor: 'pointer',
}}
space={'$8'}
onPress={onClick}
>
<Stack>
<Text size={19} weight={'semibold'}>
{name}
</Text>
</Stack>
<Icon src={icon} width={42} height={52} />
</YStack>
<Stack>
<Text size={19} weight={'semibold'}>
{name}
</Text>
<Icon src={icon} width={90} height={110} />
</Stack>
)
}

View File

@ -1,41 +1,30 @@
import { XStack } from 'tamagui'
import OSCard from './OSCard'
import { LINUX, MAC, WINDOWS } from '../../../../constants'
import styles from './InstallLayout.module.css'
const cards = [
{
name: MAC,
icon: '/icons/apple-logo.svg',
},
{
name: LINUX,
icon: '/icons/linux-logo.svg',
},
{
name: WINDOWS,
icon: '/icons/windows-logo.svg',
},
{ name: MAC, icon: '/icons/apple-logo.svg' },
{ name: LINUX, icon: '/icons/linux-logo.svg' },
{ name: WINDOWS, icon: '/icons/windows-logo.svg' },
]
type OSCardsProps = {
selectedOS: string
handleOSCardClick: (os: string) => void
}
const OSCards = ({ selectedOS, handleOSCardClick }: OSCardsProps) => {
return (
<XStack justifyContent={'space-between'} my={'15px'}>
<div className={styles.osCardsContainer}>
{cards.map(card => (
<OSCard
<div
key={card.name}
icon={card.icon}
name={card.name}
isSelected={selectedOS === card.name}
className={`${styles.osCard} ${selectedOS === card.name ? styles.osCardSelected : ''}`}
onClick={() => handleOSCardClick(card.name)}
/>
>
<OSCard key={card.name} icon={card.icon} name={card.name} />
</div>
))}
</XStack>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { YStack } from 'tamagui'
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import { useSelector } from 'react-redux'
import { useState } from 'react'
@ -18,7 +18,7 @@ const ValidatorSetupInstall = () => {
}
return (
<YStack style={{ width: '100%', padding: '26px 32px' }}>
<YStack style={{ padding: '26px 32px', width: 'fit-content' }}>
<Text size={27} weight={'semibold'}>
Validator Setup
</Text>
@ -35,9 +35,11 @@ const ValidatorSetupInstall = () => {
<Text size={19} weight={'semibold'}>
Installing {selectedClient}
</Text>
<Markdown children={DOCUMENTATIONS[selectedClient].general} />
<OSCards selectedOS={selectedOS} handleOSCardClick={handleOSCardClick} />
<Markdown children={DOCUMENTATIONS[selectedClient].documentation[selectedOS]} />
<Stack>
<Markdown children={DOCUMENTATIONS[selectedClient].general} />
<OSCards selectedOS={selectedOS} handleOSCardClick={handleOSCardClick} />
<Markdown children={DOCUMENTATIONS[selectedClient].documentation[selectedOS]} />
</Stack>
</YStack>
</YStack>
)

View File

@ -28,8 +28,9 @@ const ExecClientCard = ({ name, icon, isComingSoon }: ExecClientCardProps) => {
: '1px solid #DCE0E5',
borderRadius: '16px',
padding: '12px 16px',
width: '19%',
cursor: 'pointer',
width: '100%',
minWidth: '150px',
}}
space={'$8'}
onClick={() => {
@ -49,7 +50,6 @@ const ExecClientCard = ({ name, icon, isComingSoon }: ExecClientCardProps) => {
alignItems: 'center',
padding: '3px 6px',
borderRadius: '67px',
width: 'fit-content',
}}
>
<Text size={11} color="#fff">

View File

@ -1,4 +1,4 @@
import { Stack, XStack, YStack } from 'tamagui'
import { Stack, YStack } from 'tamagui'
import { Text } from '@status-im/components'
import ExecClientCard from './ExecClientCard'
@ -10,11 +10,17 @@ const ExecClientCards = () => {
<Stack style={{ marginTop: '15px', marginLeft: 0, marginBottom: '15px' }}>
<Text size={27}>Select Execution client</Text>
</Stack>
<XStack justifyContent={'space-between'}>
<Stack
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '8px',
}}
>
{Object.entries(DOCUMENTATIONS).map(([name, { icon }], index) => (
<ExecClientCard key={index} name={name} icon={icon} />
))}
</XStack>
</Stack>
</YStack>
)
}

View File

@ -12,7 +12,7 @@ const ValidatorSetup = () => {
minHeight={'65vh'}
justifyContent={'space-between'}
>
<XStack justifyContent={'space-between'} alignItems={'center'}>
<XStack justifyContent={'space-between'} alignItems={'center'} flexWrap="wrap" space={'$8'}>
<Text size={27} weight={'semibold'}>
Validator Setup
</Text>

View File

@ -1,4 +1,4 @@
.gradient-wrapper .image-container .background-img {
.gradientWrapper .image-container .background-img {
position: absolute;
top: 50%;
left: 75%;
@ -6,7 +6,7 @@
width: auto;
}
.gradient-wrapper:after {
.gradientWrapper:after {
display: block;
content: '';
position: absolute;

View File

@ -68,3 +68,7 @@ export const getHeightPercentages = (amountOfElements: number) => {
return `${percentages}%`
}
export const copyFunction = (text: string) => {
navigator.clipboard.writeText(text)
}