Merge pull request #14 from nimbus-gui/hn.validator-onboarding
Create validator onboarding
|
@ -2,23 +2,23 @@ import React from 'react'
|
||||||
import type { Preview } from '@storybook/react'
|
import type { Preview } from '@storybook/react'
|
||||||
import { TamaguiProvider } from '@tamagui/web'
|
import { TamaguiProvider } from '@tamagui/web'
|
||||||
import { Provider as StatusProvider } from '@status-im/components'
|
import { Provider as StatusProvider } from '@status-im/components'
|
||||||
import '../src/index.css'
|
import { Provider as ReduxProvider } from 'react-redux'
|
||||||
|
|
||||||
import appConfig from '../tamagui.config'
|
import appConfig from '../tamagui.config'
|
||||||
|
import store from '../src/redux/store'
|
||||||
|
import '../src/index.css'
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
|
||||||
// layout: 'centered',
|
|
||||||
},
|
|
||||||
decorators: [
|
decorators: [
|
||||||
Story => {
|
Story => (
|
||||||
return (
|
<TamaguiProvider config={appConfig}>
|
||||||
<TamaguiProvider config={appConfig}>
|
<StatusProvider>
|
||||||
<StatusProvider>
|
<ReduxProvider store={store}>
|
||||||
<Story />
|
<Story />
|
||||||
</StatusProvider>
|
</ReduxProvider>
|
||||||
</TamaguiProvider>
|
</StatusProvider>
|
||||||
)
|
</TamaguiProvider>
|
||||||
},
|
),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
package.json
|
@ -22,7 +22,8 @@
|
||||||
"@nivo/pie": "^0.83.0",
|
"@nivo/pie": "^0.83.0",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@status-im/colors": "*",
|
"@status-im/colors": "*",
|
||||||
"@status-im/components": "^0.2.6",
|
"@status-im/components": "^0.3.0",
|
||||||
|
"@storybook/addon-actions": "^7.4.0",
|
||||||
"@tamagui/config": "1.36.4",
|
"@tamagui/config": "1.36.4",
|
||||||
"@tamagui/react-17-patch": "1.36.4",
|
"@tamagui/react-17-patch": "1.36.4",
|
||||||
"@tamagui/vite-plugin": "1.36.4",
|
"@tamagui/vite-plugin": "1.36.4",
|
||||||
|
@ -32,12 +33,16 @@
|
||||||
"expo-modules-core": "^1.5.9",
|
"expo-modules-core": "^1.5.9",
|
||||||
"react": "18",
|
"react": "18",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
|
"react-confetti": "^6.1.0",
|
||||||
"react-dom": "18",
|
"react-dom": "18",
|
||||||
|
"react-form-stepper": "^2.0.3",
|
||||||
"react-native": "^0.72.3",
|
"react-native": "^0.72.3",
|
||||||
"react-native-svg": "^13.10.0",
|
"react-native-svg": "^13.10.0",
|
||||||
"react-redux": "^8.1.2",
|
"react-redux": "^8.1.2",
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"tamagui": "1.36.4"
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"tamagui": "1.36.4",
|
||||||
|
"web-bip39": "^0.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fsouza/prettierd": "^0.24.2",
|
"@fsouza/prettierd": "^0.24.2",
|
||||||
|
@ -52,6 +57,7 @@
|
||||||
"@storybook/test-runner": "^0.12.0",
|
"@storybook/test-runner": "^0.12.0",
|
||||||
"@storybook/testing-library": "^0.2.0",
|
"@storybook/testing-library": "^0.2.0",
|
||||||
"@types/react-color": "^3.0.6",
|
"@types/react-color": "^3.0.6",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.7",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
|
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 496 B |
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="74" height="60" viewBox="0 0 74 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Frame 31443">
|
||||||
|
<path id="Vector" d="M59.1019 45.0375V59.0747C51.3854 59.0747 47.4072 58.373 42.7293 52.0562C38.0514 45.7393 35.0293 45.0373 31.0346 45.0373V59.0747H17.001V45.0373H31.0346V31C38.2347 31 42.7293 31.9358 47.4072 38.0187C52.0851 44.1016 54.5517 45.0375 59.1019 45.0375V31H73.1355V45.0375H59.1019Z" fill="#DCE0E5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 441 B |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.1 KiB |
|
@ -12,6 +12,7 @@ import { useSelector } from 'react-redux'
|
||||||
import PinnedNotification from './components/General/PinnedNottification'
|
import PinnedNotification from './components/General/PinnedNottification'
|
||||||
import { RootState } from './redux/store'
|
import { RootState } from './redux/store'
|
||||||
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
|
import CreateLocalNodePage from './pages/CreateLocalNodePage/CreateLocalNodePage'
|
||||||
|
import ValidatorOnboarding from './pages/ValidatorOnboarding/ValidatorOnboarding'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
@ -35,6 +36,7 @@ const router = createBrowserRouter([
|
||||||
element: <PairDevice />,
|
element: <PairDevice />,
|
||||||
},
|
},
|
||||||
{ path: '/create-local-node', element: <CreateLocalNodePage /> },
|
{ path: '/create-local-node', element: <CreateLocalNodePage /> },
|
||||||
|
{ path: '/validator-onboarding', element: <ValidatorOnboarding /> },
|
||||||
])
|
])
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import BorderBox from './BorderBox'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'General/BorderBox',
|
||||||
|
component: BorderBox,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof BorderBox>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const WithData: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'BorderBox',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutData: Story = {
|
||||||
|
args: {
|
||||||
|
children: '',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Stack } from 'tamagui'
|
||||||
|
|
||||||
|
type BorderBoxProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
style?: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const BorderBox = ({ children, style }: BorderBoxProps) => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
style={{ border: '1px solid #DCE0E5', borderRadius: '16px', padding: '6px 12px', ...style }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BorderBox
|
|
@ -7,7 +7,7 @@ type LabelInputProps = {
|
||||||
placeholderText: string
|
placeholderText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function LabelInputField({ labelText, placeholderText }: LabelInputProps) {
|
const LabelInputField = ({ labelText, placeholderText }: LabelInputProps) => {
|
||||||
return (
|
return (
|
||||||
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
|
<Label flexDirection="column" alignItems="flex-start" my={10} width={'100%'}>
|
||||||
<Text size={13} weight="regular" color={'#647084'}>
|
<Text size={13} weight="regular" color={'#647084'}>
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import LinkWithArrow from './LinkWithArrow'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'General/LinkWithArrow',
|
||||||
|
component: LinkWithArrow,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof LinkWithArrow>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const LeftArrow: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Learn More',
|
||||||
|
to: '/',
|
||||||
|
arrowLeft: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RightArrow: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Learn More',
|
||||||
|
to: '/',
|
||||||
|
arrowRight: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BothArrows: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Learn More',
|
||||||
|
to: '/',
|
||||||
|
arrowLeft: true,
|
||||||
|
arrowRight: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutArrow: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Learn More',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutText: Story = {
|
||||||
|
args: {
|
||||||
|
text: '',
|
||||||
|
to: '/',
|
||||||
|
arrowLeft: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithLongText: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'This is a very long text that is used to test the component.',
|
||||||
|
to: '/',
|
||||||
|
arrowLeft: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithCustomStyle: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Learn More',
|
||||||
|
to: '/',
|
||||||
|
arrowLeft: true,
|
||||||
|
style: { backgroundColor: 'lightgray' },
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { XStack } from 'tamagui'
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from '@status-im/icons'
|
||||||
|
|
||||||
|
type LinkWithArrowProps = {
|
||||||
|
text: string
|
||||||
|
to: string
|
||||||
|
arrowLeft?: boolean
|
||||||
|
arrowRight?: boolean
|
||||||
|
style?: React.CSSProperties
|
||||||
|
textColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkWithArrow = ({
|
||||||
|
text,
|
||||||
|
to,
|
||||||
|
arrowLeft,
|
||||||
|
arrowRight,
|
||||||
|
style,
|
||||||
|
textColor,
|
||||||
|
}: LinkWithArrowProps) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const navigateHandler = () => {
|
||||||
|
navigate(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XStack
|
||||||
|
space={'$1.5'}
|
||||||
|
style={{
|
||||||
|
alignItems: 'center',
|
||||||
|
maxWidth: 'fit-content',
|
||||||
|
cursor: 'pointer',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
onClick={navigateHandler}
|
||||||
|
>
|
||||||
|
{arrowLeft && <ArrowLeftIcon size={20} color="#2A4CF4" />}
|
||||||
|
<Link style={{ color: textColor || '#2A4CF4', marginBottom: '2px' }} to={to}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
{arrowRight && <ArrowRightIcon size={20} color="#2A4CF4" />}
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LinkWithArrow
|
|
@ -1,16 +0,0 @@
|
||||||
import { Text } from '@status-im/components'
|
|
||||||
|
|
||||||
type SubTitleProps = {
|
|
||||||
color?: string
|
|
||||||
children: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SubTitle = ({ color, children }: SubTitleProps) => {
|
|
||||||
return (
|
|
||||||
<Text size={15} color={color}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubTitle
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { Text } from '@status-im/components'
|
|
||||||
|
|
||||||
type TitleProps = {
|
|
||||||
color?: string
|
|
||||||
children: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Title = ({ color, children }: TitleProps) => {
|
|
||||||
return (
|
|
||||||
<Text size={27} weight={'semibold'} color={color}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Title
|
|
|
@ -1,28 +1,35 @@
|
||||||
import { XStack, YStack } from 'tamagui'
|
import { XStack, YStack } from 'tamagui'
|
||||||
import { Button, Text } from '@status-im/components'
|
import { Button, Text } from '@status-im/components'
|
||||||
import Title from './Title'
|
|
||||||
import { RevealIcon } from '@status-im/icons'
|
import { RevealIcon } from '@status-im/icons'
|
||||||
|
|
||||||
type TitlesProps = {
|
type TitlesProps = {
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
isAdvancedSettings?: boolean
|
isAdvancedSettings?: boolean
|
||||||
|
titleSize?: 27 | 15 | 11 | 13 | 19
|
||||||
|
subtitleSize?: 27 | 15 | 11 | 13 | 19
|
||||||
}
|
}
|
||||||
|
|
||||||
const Titles = ({ title, subtitle, isAdvancedSettings }: TitlesProps) => {
|
const Titles = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
isAdvancedSettings,
|
||||||
|
titleSize = 27,
|
||||||
|
subtitleSize = 15,
|
||||||
|
}: TitlesProps) => {
|
||||||
return (
|
return (
|
||||||
<YStack style={{ width: '100%', margin: '0 0 1em' }}>
|
<YStack style={{ width: '100%', margin: '0 0 1em' }}>
|
||||||
<XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
<XStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Title color={'#09101C'}>{title}</Title>
|
<Text size={titleSize} weight={'semibold'}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
{isAdvancedSettings && (
|
{isAdvancedSettings && (
|
||||||
<Button size={32} variant="outline" icon={<RevealIcon size={20} />}>
|
<Button size={32} variant="outline" icon={<RevealIcon size={20} />}>
|
||||||
Advanced Settings
|
Advanced Settings
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</XStack>
|
</XStack>
|
||||||
<Text size={15} weight="regular">
|
<Text size={subtitleSize}>{subtitle}</Text>
|
||||||
{subtitle}
|
|
||||||
</Text>
|
|
||||||
</YStack>
|
</YStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ const PageWrapperShadow = ({
|
||||||
<div className="container-inner">{children}</div>
|
<div className="container-inner">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="layout-right">
|
<section className="layout-right">
|
||||||
<div className="image-container">
|
<div className="image-container">
|
||||||
<img src={rightImageSrc} alt="background" className="background-img" />
|
<img src={rightImageSrc} alt="background" className="background-img" />
|
||||||
|
|
|
@ -9,3 +9,9 @@ export const BAD_STORAGE_TEXT =
|
||||||
export const BAD_CPU_CLOCK_RATE_TEXT = 'Your CPU clock rate is below the recommended 2.4GHz.'
|
export const BAD_CPU_CLOCK_RATE_TEXT = 'Your CPU clock rate is below the recommended 2.4GHz.'
|
||||||
export const BAD_RAM_MEMORY_TEXT = 'There is insufficient RAM required for selected services.'
|
export const BAD_RAM_MEMORY_TEXT = 'There is insufficient RAM required for selected services.'
|
||||||
export const BAD_NETWORK_TEXT = 'Network Latency is high.'
|
export const BAD_NETWORK_TEXT = 'Network Latency is high.'
|
||||||
|
|
||||||
|
/* Validator Onboarding */
|
||||||
|
|
||||||
|
export const KEYSTORE_FILES = 'KeystoreFiles'
|
||||||
|
export const RECOVERY_PHRASE = 'Recovery Phrase'
|
||||||
|
export const BOTH_KEY_AND_RECOVERY = 'Both KeystoreFiles & Recovery Phrase'
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client'
|
||||||
import { Provider as ReduxProvider } from 'react-redux'
|
import { Provider as ReduxProvider } from 'react-redux'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import store from './redux/store.tsx'
|
import store from './redux/store.ts'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof ConnectDevicePage>
|
} satisfies Meta<typeof ConnectDevicePage>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof CreateLocalNodePage>
|
} satisfies Meta<typeof CreateLocalNodePage>
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
import { Provider as ReduxProvider } from 'react-redux'
|
|
||||||
import store from '../../redux/store'
|
|
||||||
import DeviceHealthCheck from './DeviceHealthCheck'
|
import DeviceHealthCheck from './DeviceHealthCheck'
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Pages/DeviceHealthCheck',
|
title: 'Pages/DeviceHealthCheck',
|
||||||
component: DeviceHealthCheck,
|
component: DeviceHealthCheck,
|
||||||
decorators: [
|
decorators: [],
|
||||||
StoryObj => (
|
|
||||||
<ReduxProvider store={store}>
|
|
||||||
<StoryObj />
|
|
||||||
</ReduxProvider>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
import { Provider as ReduxProvider } from 'react-redux'
|
|
||||||
import store from '../../redux/store'
|
|
||||||
|
|
||||||
import DeviceSyncStatus from './DeviceSyncStatus'
|
import DeviceSyncStatus from './DeviceSyncStatus'
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
|
@ -13,11 +11,9 @@ const meta = {
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
decorators: [
|
decorators: [
|
||||||
Story => (
|
Story => (
|
||||||
<ReduxProvider store={store}>
|
<div style={{ height: '100%', width: '100%' }}>
|
||||||
<div style={{ height: '100%', width: '100%' }}>
|
<Story />
|
||||||
<Story />
|
</div>
|
||||||
</div>
|
|
||||||
</ReduxProvider>
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
} satisfies Meta<typeof DeviceSyncStatus>
|
} satisfies Meta<typeof DeviceSyncStatus>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { useEffect } from 'react'
|
||||||
|
|
||||||
const DeviceSyncStatus = () => {
|
const DeviceSyncStatus = () => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setPinnedMessage({
|
setPinnedMessage({
|
||||||
|
@ -22,6 +23,7 @@ const DeviceSyncStatus = () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapperShadow rightImageSrc="./background-images/sync-status-background.png">
|
<PageWrapperShadow rightImageSrc="./background-images/sync-status-background.png">
|
||||||
<YStack
|
<YStack
|
||||||
|
@ -38,8 +40,8 @@ const DeviceSyncStatus = () => {
|
||||||
subtitle="Monitor your Validator Client and Beacon Node syncing progression."
|
subtitle="Monitor your Validator Client and Beacon Node syncing progression."
|
||||||
/>
|
/>
|
||||||
<YStack style={{ width: '100%' }}>
|
<YStack style={{ width: '100%' }}>
|
||||||
<SyncStatusCardExecution synced={132432} total={200000} />
|
<SyncStatusCardExecution synced={132.432} total={200.0} />
|
||||||
<SyncStatusCardConsensus synced={149500} total={160000} />
|
<SyncStatusCardConsensus synced={149.5} total={160.0} />
|
||||||
</YStack>
|
</YStack>
|
||||||
<Stack style={{ marginTop: '1rem' }}>
|
<Stack style={{ marginTop: '1rem' }}>
|
||||||
<Button>Continue</Button>
|
<Button>Continue</Button>
|
||||||
|
|
|
@ -5,33 +5,31 @@ import Icon from '../../components/General/Icon'
|
||||||
import StandardGauge from '../../components/Charts/StandardGauge'
|
import StandardGauge from '../../components/Charts/StandardGauge'
|
||||||
import IconText from '../../components/General/IconText'
|
import IconText from '../../components/General/IconText'
|
||||||
import { TokenIcon } from '@status-im/icons'
|
import { TokenIcon } from '@status-im/icons'
|
||||||
|
import { formatNumberForGauge } from '../../utilities'
|
||||||
|
|
||||||
interface DeviceStorageHealthProps {
|
interface DeviceStorageHealthProps {
|
||||||
synced: number
|
synced: number
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, total }) => {
|
const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, total }) => {
|
||||||
const message = synced === total ? 'Synced all data' : 'Syncing'
|
const message = synced === total ? 'Synced all data' : 'Syncing'
|
||||||
|
|
||||||
const data = () => {
|
const data = [
|
||||||
return [
|
{
|
||||||
{
|
id: 'storage',
|
||||||
id: 'storage',
|
label: 'Used',
|
||||||
label: 'Used',
|
value: synced,
|
||||||
value: synced,
|
color: '#ff6161',
|
||||||
color: '#ff6161',
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'storage',
|
||||||
id: 'storage',
|
label: 'Free',
|
||||||
label: 'Free',
|
value: total - synced || 1,
|
||||||
value: total - synced || 1,
|
color: '#E7EAEE',
|
||||||
color: '#E7EAEE',
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
const formatNumber = (n: number): string => {
|
|
||||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Shadow
|
<Shadow
|
||||||
variant="$2"
|
variant="$2"
|
||||||
|
@ -63,7 +61,7 @@ const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, t
|
||||||
width: '115px',
|
width: '115px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StandardGauge data={data()} />
|
<StandardGauge data={data} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
@ -74,7 +72,7 @@ const SyncStatusCardConsensus: React.FC<DeviceStorageHealthProps> = ({ synced, t
|
||||||
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
|
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
|
||||||
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
|
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
|
||||||
<Text size={13}>
|
<Text size={13}>
|
||||||
{formatNumber(synced)} / {formatNumber(total)}
|
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Shadow, Text } from '@status-im/components'
|
||||||
import StandardGauge from '../../components/Charts/StandardGauge'
|
import StandardGauge from '../../components/Charts/StandardGauge'
|
||||||
import IconText from '../../components/General/IconText'
|
import IconText from '../../components/General/IconText'
|
||||||
import { TokenIcon } from '@status-im/icons'
|
import { TokenIcon } from '@status-im/icons'
|
||||||
|
import { formatNumberForGauge } from '../../utilities'
|
||||||
|
|
||||||
interface DeviceStorageHealthProps {
|
interface DeviceStorageHealthProps {
|
||||||
synced: number
|
synced: number
|
||||||
|
@ -27,9 +28,6 @@ const SyncStatusCardExecution: React.FC<DeviceStorageHealthProps> = ({ synced, t
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const formatNumber = (n: number): string => {
|
|
||||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shadow
|
<Shadow
|
||||||
|
@ -75,8 +73,7 @@ const SyncStatusCardExecution: React.FC<DeviceStorageHealthProps> = ({ synced, t
|
||||||
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
|
<XStack space={'$2'} style={{ padding: '10px 16px 10px 16px' }}>
|
||||||
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
|
<IconText icon={<TokenIcon size={16} />}>{message}</IconText>
|
||||||
<Text size={13}>
|
<Text size={13}>
|
||||||
{' '}
|
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
|
||||||
{formatNumber(synced)} / {formatNumber(total)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof LandingPage>
|
} satisfies Meta<typeof LandingPage>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import './LandingPage.css'
|
import './LandingPage.css'
|
||||||
import { XStack, YStack } from 'tamagui'
|
import { XStack, YStack } from 'tamagui'
|
||||||
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
import PageWrapperShadow from '../../components/PageWrappers/PageWrapperShadow'
|
||||||
import Title from '../../components/General/Title'
|
|
||||||
import NimbusLogo from '../../components/Logos/NimbusLogo'
|
import NimbusLogo from '../../components/Logos/NimbusLogo'
|
||||||
import { NodeIcon } from '@status-im/icons'
|
import { NodeIcon } from '@status-im/icons'
|
||||||
import { Button as StatusButton, Text } from '@status-im/components'
|
import { Button as StatusButton, Text } from '@status-im/components'
|
||||||
|
@ -23,10 +22,10 @@ const LandingPage = () => {
|
||||||
<NimbusLogo />
|
<NimbusLogo />
|
||||||
</XStack>
|
</XStack>
|
||||||
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }}>
|
<YStack style={{ width: '100%', margin: '30vh 0 4vh' }}>
|
||||||
<Title color="$textPrimary">
|
<Text size={27} weight={'semibold'}>
|
||||||
Light and performant clients, for all Ethereum validators.
|
Light and performant clients, for all Ethereum validators.
|
||||||
</Title>
|
</Text>
|
||||||
<Text size={15} weight="regular" color="$textPrimary">
|
<Text size={15} weight="regular">
|
||||||
<strong>Nimbus Nodes</strong> allows you to take control and ownership of the services
|
<strong>Nimbus Nodes</strong> allows you to take control and ownership of the services
|
||||||
you wish to run in a completely trustless and decentralized manner.
|
you wish to run in a completely trustless and decentralized manner.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof GenerateId>
|
} satisfies Meta<typeof GenerateId>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof PairDevice>
|
} satisfies Meta<typeof PairDevice>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
} satisfies Meta<typeof PairedSuccessfully>
|
} satisfies Meta<typeof PairedSuccessfully>
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
|
|
|
@ -10,14 +10,13 @@ const meta = {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {},
|
|
||||||
decorators: [withRouter],
|
decorators: [withRouter],
|
||||||
} satisfies Meta<typeof SyncStatus>
|
} satisfies Meta<typeof SyncStatus>
|
||||||
|
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
export const Page: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
isPairing: true,
|
isPairing: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import Activation from './Activation'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/Activation',
|
||||||
|
component: Activation,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof Activation>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
validatorsValue: '4',
|
||||||
|
executionSyncStatus1: {
|
||||||
|
text: "Execution Sync Status",
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: "$blue",
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
|
||||||
|
},
|
||||||
|
executionSyncStatus2: {
|
||||||
|
text: "Execution Sync Status",
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: "$red",
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
currentAPRValue: "4.40%",
|
||||||
|
estimatedActivationTimeValue: "32 Days",
|
||||||
|
validatorQueueValue: "92603",
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { XStack, Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import Confetti from 'react-confetti'
|
||||||
|
|
||||||
|
import ActivationCard from './ActivationCard'
|
||||||
|
import LinkWithArrow from '../../../components/General/LinkWithArrow'
|
||||||
|
|
||||||
|
type ActivationProps = {
|
||||||
|
validatorsValue: string
|
||||||
|
executionSyncStatus1: {
|
||||||
|
text: string
|
||||||
|
isGaugeIncluded: boolean
|
||||||
|
gaugeColor: string
|
||||||
|
gaugeSynced: number
|
||||||
|
gaugeTotal: number
|
||||||
|
}
|
||||||
|
executionSyncStatus2: {
|
||||||
|
text: string
|
||||||
|
isGaugeIncluded: boolean
|
||||||
|
gaugeColor: string
|
||||||
|
gaugeSynced: number
|
||||||
|
gaugeTotal: number
|
||||||
|
}
|
||||||
|
currentAPRValue: string
|
||||||
|
estimatedActivationTimeValue: string
|
||||||
|
validatorQueueValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Activation = ({
|
||||||
|
validatorsValue,
|
||||||
|
executionSyncStatus1,
|
||||||
|
executionSyncStatus2,
|
||||||
|
currentAPRValue,
|
||||||
|
estimatedActivationTimeValue,
|
||||||
|
validatorQueueValue,
|
||||||
|
}: ActivationProps) => {
|
||||||
|
const [showConfetti, setShowConfetti] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowConfetti(false)
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={styles.confettiContainer} width={'100%'}>
|
||||||
|
{showConfetti && <Confetti style={styles.confettiCanvas} />}
|
||||||
|
<YStack style={{ padding: '16px 32px' }}>
|
||||||
|
<YStack space={'$5'}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
Activation
|
||||||
|
</Text>
|
||||||
|
<Stack style={{ width: '66%' }}>
|
||||||
|
<Text size={27}>
|
||||||
|
Congratulations! You have successfully setup your Nimbus Validators and are currently
|
||||||
|
syncing your nodes.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<YStack space={'$3'} marginTop={'10px'} width={'33%'}>
|
||||||
|
<XStack space={'$3'} justifyContent={'space-between'}>
|
||||||
|
<ActivationCard text="Validators" value={validatorsValue} />
|
||||||
|
<ActivationCard {...executionSyncStatus1} />
|
||||||
|
<ActivationCard {...executionSyncStatus2} />
|
||||||
|
</XStack>
|
||||||
|
<XStack space={'$3'}>
|
||||||
|
<ActivationCard text="Current APR" value={currentAPRValue} />
|
||||||
|
<ActivationCard
|
||||||
|
text="Estimated Activation Time"
|
||||||
|
value={estimatedActivationTimeValue}
|
||||||
|
/>
|
||||||
|
<ActivationCard text="Validator Queue" value={validatorQueueValue} />
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
<LinkWithArrow
|
||||||
|
text="Edit Validators"
|
||||||
|
to="/"
|
||||||
|
arrowLeft={true}
|
||||||
|
style={{ marginTop: '44px', marginBottom: '88px' }}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Activation
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
confettiContainer: {
|
||||||
|
position: 'relative' as const,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
confettiCanvas: {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 0,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ActivationCard from './ActivationCard'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ActivationCard',
|
||||||
|
component: ActivationCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof ActivationCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Validators: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Validators',
|
||||||
|
value: '4',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExecutionSyncStatus: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Execution Sync Status',
|
||||||
|
value: '',
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExecutionSyncStatusRed: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Execution Sync Status',
|
||||||
|
value: '',
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: '#EB5757',
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CurrentAPR: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Current APR',
|
||||||
|
value: '4.40%',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EstimatedActivationTime: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Estimated Activation Time',
|
||||||
|
value: '32 Days',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorQueue: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Validator Queue',
|
||||||
|
value: '92603',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
text: '',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutValue: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Title',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import ActivationSyncCard from './ActivationSyncCard'
|
||||||
|
|
||||||
|
type ActivationCardProps = {
|
||||||
|
text: string
|
||||||
|
value?: string
|
||||||
|
isGaugeIncluded?: boolean
|
||||||
|
gaugeColor?: string
|
||||||
|
gaugeSynced?: number
|
||||||
|
gaugeTotal?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivationCard = ({
|
||||||
|
text,
|
||||||
|
value,
|
||||||
|
isGaugeIncluded,
|
||||||
|
gaugeColor,
|
||||||
|
gaugeSynced,
|
||||||
|
gaugeTotal,
|
||||||
|
}: ActivationCardProps) => {
|
||||||
|
return (
|
||||||
|
<YStack
|
||||||
|
style={{
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.15)',
|
||||||
|
padding: '12px 16px',
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isGaugeIncluded && (
|
||||||
|
<Stack>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
<Text size={27} color="blue" weight={'semibold'}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{isGaugeIncluded && (
|
||||||
|
<Stack>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
<ActivationSyncCard
|
||||||
|
gaugeColor={gaugeColor || ''}
|
||||||
|
gaugeSynced={gaugeSynced || 0}
|
||||||
|
gaugeTotal={gaugeTotal || 1}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActivationCard
|
|
@ -0,0 +1,65 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import ActivationSyncCard from './ActivationSyncCard'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ActivationSyncCard',
|
||||||
|
component: ActivationSyncCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof ActivationSyncCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Blue: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Red: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#EB5757',
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxValue: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: 172.503,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverMaxValue: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: 200,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MinValue: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: 0,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnderMinValue: Story = {
|
||||||
|
args: {
|
||||||
|
gaugeColor: '#2a4af5',
|
||||||
|
gaugeSynced: -100,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import StandardGauge from '../../../components/Charts/StandardGauge'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { formatNumberForGauge } from '../../../utilities'
|
||||||
|
|
||||||
|
type ActivationSyncCardProps = {
|
||||||
|
gaugeColor: string
|
||||||
|
gaugeSynced: number
|
||||||
|
gaugeTotal: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivationSyncCard = ({ gaugeColor, gaugeSynced, gaugeTotal }: ActivationSyncCardProps) => {
|
||||||
|
return (
|
||||||
|
<XStack space={'$2'} alignItems="center">
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StandardGauge
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
id: 'sync card',
|
||||||
|
label: 'Sync Status',
|
||||||
|
value: gaugeSynced,
|
||||||
|
color: gaugeColor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
label: 'free',
|
||||||
|
value: gaugeTotal - gaugeSynced || 1,
|
||||||
|
color: '#E7EAEE',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<YStack>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{formatNumberForGauge(gaugeSynced)} / {formatNumberForGauge(gaugeTotal)}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActivationSyncCard
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import Advisories from './Advisories'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/Advisories',
|
||||||
|
component: Advisories,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof Advisories>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
|
||||||
|
import AdvisoriesContent from './AdvisoriesContent'
|
||||||
|
|
||||||
|
type AdvisoryTopicsType = {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Advisories = () => {
|
||||||
|
const [selectedTitle, setSelectedTitle] = useState(Object.keys(advisoryTopics)[3])
|
||||||
|
|
||||||
|
const isSameTitle = (title: string) => selectedTitle === title
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XStack
|
||||||
|
style={{ padding: '16px 32px', justifyContent: 'space-between' }}
|
||||||
|
height={'65vh'}
|
||||||
|
width={'100%'}
|
||||||
|
>
|
||||||
|
<YStack space={'$2'}>
|
||||||
|
<Stack marginBottom="$6">
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
Advisories
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
{Object.keys(advisoryTopics).map((title, index) => (
|
||||||
|
<XStack
|
||||||
|
key={title}
|
||||||
|
onPress={() => setSelectedTitle(title)}
|
||||||
|
style={{ cursor: 'pointer', alignItems: 'center' }}
|
||||||
|
space={'$2'}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size={27}
|
||||||
|
weight={isSameTitle(title) && 'semibold'}
|
||||||
|
color={isSameTitle(title) ? 'blue' : ''}
|
||||||
|
>
|
||||||
|
{unicodeNumbers[index]}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size={19}
|
||||||
|
weight={isSameTitle(title) && 'semibold'}
|
||||||
|
color={isSameTitle(title) ? 'blue' : ''}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
))}
|
||||||
|
</YStack>
|
||||||
|
<AdvisoriesContent title={selectedTitle} content={advisoryTopics[selectedTitle]} />
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Advisories
|
||||||
|
|
||||||
|
const unicodeNumbers = ['➀', '➁', '➂', '➃', '➄', '➅']
|
||||||
|
|
||||||
|
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.',
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import AdvisoriesContent from './AdvisoriesContent'
|
||||||
|
import { advisoryTopics } from './Advisories'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/AdvisoriesContent',
|
||||||
|
component: AdvisoriesContent,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof AdvisoriesContent>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
const advisoryTopicsKeys = Object.keys(advisoryTopics)
|
||||||
|
const advisoryTopicsValues = Object.values(advisoryTopics)
|
||||||
|
|
||||||
|
export const ProofOfStake: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[0],
|
||||||
|
content: advisoryTopicsValues[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Deposit: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[1],
|
||||||
|
content: advisoryTopicsValues[1],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeyManagement: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[2],
|
||||||
|
content: advisoryTopicsValues[2],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BadBehaviour: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[3],
|
||||||
|
content: advisoryTopicsValues[3],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Requirements: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[4],
|
||||||
|
content: advisoryTopicsValues[4],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Risks: Story = {
|
||||||
|
args: {
|
||||||
|
title: advisoryTopicsKeys[5],
|
||||||
|
content: advisoryTopicsValues[5],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
|
||||||
|
type AdvisoriesContentProps = {
|
||||||
|
title: string
|
||||||
|
content: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdvisoriesContent = ({ title, content }: AdvisoriesContentProps) => {
|
||||||
|
return (
|
||||||
|
<YStack space={'$1'} style={{ width: '70%' }}>
|
||||||
|
<Stack style={{ marginBottom: '5%' }}>
|
||||||
|
<Text size={27} weight={400}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<YStack space={'$4'}>
|
||||||
|
{content.map(row => (
|
||||||
|
<Text key={row} size={19}>
|
||||||
|
{row}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
<Text size={19}>
|
||||||
|
<Link
|
||||||
|
to={'https://github.com/ethereum/consensus-specs'}
|
||||||
|
style={{ textDecorationLine: 'underline', color: '#484848' }}
|
||||||
|
>
|
||||||
|
The Ethereum consensus layer specification
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
<Link
|
||||||
|
to={'https://github.com/ethereum/consensus-specs'}
|
||||||
|
style={{ textDecorationLine: 'underline', color: '#2A4CF4', fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
More on slashing risks
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvisoriesContent
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import ClientSetup from './ClientSetup'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ClientSetup',
|
||||||
|
component: ClientSetup,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof ClientSetup>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Separator, YStack } from 'tamagui'
|
||||||
|
|
||||||
|
import SetupRow from './SetupRow'
|
||||||
|
import WithdrawalAddress from './WithdrawalAddress'
|
||||||
|
import LinkWithArrow from '../../../components/General/LinkWithArrow'
|
||||||
|
|
||||||
|
const ClientSetup = () => {
|
||||||
|
return (
|
||||||
|
<YStack padding={'26px'} width={'100%'} space={'$5'}>
|
||||||
|
<SetupRow title={'Setup up Validators'} />
|
||||||
|
<Separator borderColor={'#F0F2F5'} />
|
||||||
|
<WithdrawalAddress title={'Withdrawal address'} />
|
||||||
|
<LinkWithArrow
|
||||||
|
text="Advanced Recovery Method"
|
||||||
|
to={'/'}
|
||||||
|
arrowRight={true}
|
||||||
|
style={{ marginBottom: '50px' }}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default ClientSetup
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import SetupRow from './SetupRow'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/SetupRow',
|
||||||
|
component: SetupRow,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof SetupRow>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Setup up Validators',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import { Input as StatusInput, Text } from '@status-im/components'
|
||||||
|
import { AddIcon, ChevronDownIcon } from '@status-im/icons'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type SetupRowProps = {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetupRow = ({ title }: SetupRowProps) => {
|
||||||
|
const [validatorCount, setValidatorCount] = useState(0)
|
||||||
|
|
||||||
|
const addValidatorHandler = () => {
|
||||||
|
setValidatorCount((state: number) => state + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeValidatorCountHandler = (e: any) => {
|
||||||
|
if (!isNaN(e.target.value)) {
|
||||||
|
setValidatorCount(Number(e.target.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$4'}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<XStack justifyContent={'space-between'} width={'80%'}>
|
||||||
|
<Stack space={'$2'}>
|
||||||
|
<Text size={15} weight="regular" color={'#647084'}>
|
||||||
|
How many Validators would you like to run?
|
||||||
|
</Text>
|
||||||
|
<StatusInput
|
||||||
|
icon={<AddIcon size={16} style={{ cursor: 'pointer' }} onClick={addValidatorHandler} />}
|
||||||
|
value={validatorCount.toString()}
|
||||||
|
onChange={changeValidatorCountHandler}
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<YStack space={'$2'}>
|
||||||
|
<Text size={19} weight={'semibold'} color="#09101C">
|
||||||
|
ETH
|
||||||
|
</Text>
|
||||||
|
<Text size={27} weight={'semibold'} color="#09101C">
|
||||||
|
64
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
<YStack space={'$2'}>
|
||||||
|
<XStack style={{ justifyContent: 'space-between' }}>
|
||||||
|
<Text size={19} weight={'semibold'} color="#09101C">
|
||||||
|
USD
|
||||||
|
</Text>
|
||||||
|
<ChevronDownIcon size={16} color={'#919191'} />
|
||||||
|
</XStack>
|
||||||
|
<Text size={27} weight={'semibold'} color="#09101C">
|
||||||
|
$4,273 USD
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default SetupRow
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import WithdrawalAddress from './WithdrawalAddress'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/WithdrawalAddress',
|
||||||
|
component: WithdrawalAddress,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof WithdrawalAddress>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Withdrawal Address',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
import { InformationBox, Input as StatusInput, Text } from '@status-im/components'
|
||||||
|
import { ClearIcon, CloseCircleIcon } from '@status-im/icons'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type WithdrawalAddressProps = {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const WithdrawalAddress = ({ title }: WithdrawalAddressProps) => {
|
||||||
|
const [withdrawalAddress, setWithdrawalAddress] = useState('')
|
||||||
|
|
||||||
|
const changeWithdrawalAddressHandler = (e: any) => {
|
||||||
|
setWithdrawalAddress(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeWithdrawalAddressHandler = () => {
|
||||||
|
setWithdrawalAddress('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$4'}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<YStack space={'$3'}>
|
||||||
|
<Text size={13} weight="regular" color={'#647084'}>
|
||||||
|
Ethereum Address
|
||||||
|
</Text>
|
||||||
|
<Stack width={'100%'}>
|
||||||
|
<StatusInput
|
||||||
|
placeholder={'******************'}
|
||||||
|
width={'100%'}
|
||||||
|
icon={
|
||||||
|
<ClearIcon
|
||||||
|
size={16}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={removeWithdrawalAddressHandler}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={withdrawalAddress}
|
||||||
|
onChange={changeWithdrawalAddressHandler}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<InformationBox
|
||||||
|
message="If withdrawal address is not provided at this step, your deposited funds will remain locked on the Beacon Chain until an address is provided. Unlocking will require signing a message with your withdrawal keys, generated from your mnemonic seed phrase (so keep it safe)."
|
||||||
|
variant="error"
|
||||||
|
icon={<CloseCircleIcon size={20} color="$red" />}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WithdrawalAddress
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import ContinueButton from './ContinueButton'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ContinueButton',
|
||||||
|
component: ContinueButton,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof ContinueButton>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
continueHandler: () => {},
|
||||||
|
activeStep: 0,
|
||||||
|
isConfirmPhraseStage: false,
|
||||||
|
subStepValidatorSetup: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
continueHandler: () => {},
|
||||||
|
activeStep: 0,
|
||||||
|
isConfirmPhraseStage: true,
|
||||||
|
subStepValidatorSetup: 0,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Stack, XStack } from 'tamagui'
|
||||||
|
import { Button, InformationBox } from '@status-im/components'
|
||||||
|
import { CloseCircleIcon } from '@status-im/icons'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { RootState } from '../../redux/store'
|
||||||
|
import { setIsRightPhrase } from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
|
||||||
|
import LinkWithArrow from '../../components/General/LinkWithArrow'
|
||||||
|
|
||||||
|
type ContinueButton = {
|
||||||
|
continueHandler: () => void
|
||||||
|
activeStep: number
|
||||||
|
isConfirmPhraseStage: boolean
|
||||||
|
subStepValidatorSetup: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContinueButton = ({
|
||||||
|
continueHandler,
|
||||||
|
activeStep,
|
||||||
|
isConfirmPhraseStage,
|
||||||
|
subStepValidatorSetup,
|
||||||
|
}: ContinueButton) => {
|
||||||
|
const { isCopyPastedPhrase, isRightPhrase, words, validWords } = useSelector(
|
||||||
|
(state: RootState) => state.keyGeneration,
|
||||||
|
)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setIsRightPhrase(words.every(word => word !== '')))
|
||||||
|
}, [words])
|
||||||
|
|
||||||
|
const isDisabled = () => {
|
||||||
|
if (
|
||||||
|
(isConfirmPhraseStage && !isRightPhrase) ||
|
||||||
|
(isConfirmPhraseStage && validWords.some(w => w === false))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActivationValScreen = activeStep === 3 && subStepValidatorSetup === 3
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XStack style={{ width: '100%', alignItems: 'center', zIndex: 999, marginTop: '40px' }}>
|
||||||
|
<Stack style={{ width: '100%' }}>
|
||||||
|
{isCopyPastedPhrase && (
|
||||||
|
<InformationBox
|
||||||
|
message="You have copy and pasted the entire Recovery Phrase. Please ensure you have secured it appropriately prior to continuing."
|
||||||
|
variant="error"
|
||||||
|
icon={<CloseCircleIcon size={20} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isActivationValScreen && (
|
||||||
|
<LinkWithArrow
|
||||||
|
text="Skip to Dashboard"
|
||||||
|
to="/"
|
||||||
|
arrowRight={true}
|
||||||
|
style={{ fontWeight: 'bold', zIndex: 1000 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
zIndex: 999,
|
||||||
|
alignItems: 'end',
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button onPress={continueHandler} size={40} disabled={isDisabled()}>
|
||||||
|
{activeStep < 5 ? 'Continue' : 'Continue to Dashboard'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContinueButton
|
|
@ -0,0 +1,68 @@
|
||||||
|
.custom-step {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-step.StepMain--active,
|
||||||
|
.custom-step.StepMain--completed {
|
||||||
|
background-color: #2a4cf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-step::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-step.StepMain--active::before,
|
||||||
|
.custom-step.StepMain--completed::before {
|
||||||
|
border-color: #2a4cf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-step.StepMain--active::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #2a4cf4;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-step::after {
|
||||||
|
content: attr(data-subtitle);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #A2A9B0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Overview"]::after {
|
||||||
|
left: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Advisories"]::after {
|
||||||
|
left: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Client Setup"]::after {
|
||||||
|
left: 32%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Validator Setup"]::after {
|
||||||
|
left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Key Generation"]::after {
|
||||||
|
left: 24.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-step="Activation"]::after {
|
||||||
|
left: 33%;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import FormStepper from './FormStepper'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/FormStepper',
|
||||||
|
component: FormStepper,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof FormStepper>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const OverviewActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 0,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdvisoriesActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 1,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientSetupActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 2,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorSetupActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 3,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeyGenerationActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 4,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActivationActive: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: 5,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoActiveStep: Story = {
|
||||||
|
args: {
|
||||||
|
activeStep: -1,
|
||||||
|
changeActiveStep: () => {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Stepper, Step } from 'react-form-stepper'
|
||||||
|
import './FormStepper.css'
|
||||||
|
|
||||||
|
type FormStepperProps = {
|
||||||
|
activeStep: number
|
||||||
|
changeActiveStep: (step: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'Activation', subtitle: 'Complete Setup' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const FormStepper = ({ activeStep, changeActiveStep }: FormStepperProps) => {
|
||||||
|
return (
|
||||||
|
<Stepper
|
||||||
|
activeStep={activeStep}
|
||||||
|
nonLinear={true}
|
||||||
|
styleConfig={stepStyle}
|
||||||
|
connectorStyleConfig={customConnectorStyle}
|
||||||
|
style={{ fontSize: '14px', zIndex: 999, width: '100%', padding: 0, marginBottom: '2rem' }}
|
||||||
|
>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<Step
|
||||||
|
key={index}
|
||||||
|
label={step.label}
|
||||||
|
className="custom-step"
|
||||||
|
onClick={() => changeActiveStep(index)}
|
||||||
|
completed={activeStep > index - 1}
|
||||||
|
data-subtitle={step.subtitle}
|
||||||
|
data-step={step.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const stepStyle = {
|
||||||
|
// For default dots:
|
||||||
|
inactiveBgColor: '#FFFFFF',
|
||||||
|
inactiveBorderColor: '#E0E0E0',
|
||||||
|
inactiveBorderWidth: '2px',
|
||||||
|
// For active dots:
|
||||||
|
activeBgColor: '#FFFFFF',
|
||||||
|
activeBorderColor: '#2A4CF4',
|
||||||
|
activeBorderWidth: '2px',
|
||||||
|
// For completed dots:
|
||||||
|
completedBgColor: '#2A4CF4',
|
||||||
|
activeTextColor: '#ffffff',
|
||||||
|
completedTextColor: '#ffffff',
|
||||||
|
inactiveTextColor: '#000000',
|
||||||
|
size: '20px',
|
||||||
|
circleFontSize: '10px',
|
||||||
|
labelFontSize: '14px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
fontWeight: 700,
|
||||||
|
}
|
||||||
|
|
||||||
|
const customConnectorStyle = {
|
||||||
|
size: '2px',
|
||||||
|
activeColor: '#2A4CF4',
|
||||||
|
disabledColor: '#bdbdbd',
|
||||||
|
completedColor: '#a10308',
|
||||||
|
style: 'solid',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormStepper
|
|
@ -0,0 +1,79 @@
|
||||||
|
.autocomplete-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f9fafa;
|
||||||
|
border: 2px solid #f9fafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: scroll;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: #f9fafa;
|
||||||
|
padding: 10px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #f9fafa transparent;
|
||||||
|
border: '2px solid #f9fafa';
|
||||||
|
max-height: 250px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom-left-radius: 24px;
|
||||||
|
border-bottom-right-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid #f7f8f9;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f7f8f9;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
color: #0D162566;
|
||||||
|
top: 48%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item:hover {
|
||||||
|
background-color: #f1f2f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #f1f2f4;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import AutocompleteInput from './AutocompleteInput'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/AutocompleteInput',
|
||||||
|
component: AutocompleteInput,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof AutocompleteInput>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OtherWord: Story = {
|
||||||
|
args: {
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import wordlist from 'web-bip39/wordlists/english'
|
||||||
|
|
||||||
|
import { RootState } from '../../../../redux/store'
|
||||||
|
import {
|
||||||
|
setIsCopyPastedPhrase,
|
||||||
|
setMnemonic,
|
||||||
|
setValidWords,
|
||||||
|
setWord,
|
||||||
|
} from '../../../../redux/ValidatorOnboarding/KeyGeneration/slice'
|
||||||
|
import styles from './AutocompleteInput.module.css'
|
||||||
|
|
||||||
|
type AutocompleteInputProps = {
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutocompleteInput = ({ index }: AutocompleteInputProps) => {
|
||||||
|
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||||
|
const [isFocused, setIsFocused] = useState(false)
|
||||||
|
const word = useSelector((state: RootState) => state.keyGeneration.words[index])
|
||||||
|
const isValidWord = useSelector((state: RootState) => state.keyGeneration.validWords[index])
|
||||||
|
const validWords = useSelector((state: RootState) => state.keyGeneration.validWords)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSuggestions(getNewSuggestions(word))
|
||||||
|
}, [word])
|
||||||
|
|
||||||
|
const getNewSuggestions = (word: string) => {
|
||||||
|
return wordlist.filter(w => w.startsWith(word.toLowerCase()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!isFocused) {
|
||||||
|
handleInputFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = e.target.value
|
||||||
|
const mnemonic = value.trim().split(' ').slice(0, 24)
|
||||||
|
const mnemonicLength = mnemonic.length
|
||||||
|
let newValidWords = [...validWords]
|
||||||
|
|
||||||
|
if (mnemonicLength === 1) {
|
||||||
|
dispatch(setWord({ index, word: value }))
|
||||||
|
|
||||||
|
newValidWords[index] = wordlist.includes(value) || getNewSuggestions(value).length > 0
|
||||||
|
} else if (mnemonicLength === 24) {
|
||||||
|
dispatch(setMnemonic(mnemonic))
|
||||||
|
dispatch(setIsCopyPastedPhrase(true))
|
||||||
|
|
||||||
|
mnemonic.forEach((m, i) => {
|
||||||
|
newValidWords[i] = wordlist.includes(m)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (let i = index; i < mnemonicLength + index; i++) {
|
||||||
|
const mnemonicWord = mnemonic.shift() || ''
|
||||||
|
dispatch(setWord({ index: i, word: mnemonicWord }))
|
||||||
|
newValidWords[i] = wordlist.includes(mnemonicWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setIsCopyPastedPhrase(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setValidWords(newValidWords))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSuggestionClick = (e: React.MouseEvent, suggestion: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
setIsFocused(false)
|
||||||
|
dispatch(setWord({ index, word: suggestion }))
|
||||||
|
|
||||||
|
let newValidWords = [...validWords]
|
||||||
|
newValidWords[index] = wordlist.includes(suggestion)
|
||||||
|
dispatch(setValidWords(newValidWords))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
setIsFocused(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
setIsFocused(false)
|
||||||
|
|
||||||
|
let newValidWords = [...validWords]
|
||||||
|
newValidWords[index] = wordlist.includes(word)
|
||||||
|
dispatch(setValidWords(newValidWords))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={autocompleteContainerStyle(isFocused)} className={styles['autocomplete-container']}>
|
||||||
|
<div className={styles['input-wrapper']}>
|
||||||
|
<span className={styles['input-number']}>{index + 1}.</span>
|
||||||
|
<input
|
||||||
|
className={styles['autocomplete-input']}
|
||||||
|
value={word}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
style={inputStyle(index, isFocused, isValidWord)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={isFocused ? styles['suggestion-list'] : ''}>
|
||||||
|
{isFocused &&
|
||||||
|
suggestions.map(suggestion => (
|
||||||
|
<div
|
||||||
|
key={suggestion}
|
||||||
|
className={styles['suggestion-item']}
|
||||||
|
onMouseDown={e => handleSuggestionClick(e, suggestion)}
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutocompleteInput
|
||||||
|
|
||||||
|
const inputStyle = (index: number, isFocused: boolean, isValidWord: boolean) => {
|
||||||
|
const style = {
|
||||||
|
outline: 'none',
|
||||||
|
padding: `12px 16px 12px ${index + 1 < 10 ? '35px' : '45px'}`,
|
||||||
|
border: isValidWord ? '2px solid #f7f8f9' : '2px solid #E53E3E',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
border: isValidWord ? '2px solid #4360DF' : '2px solid #E53E3E',
|
||||||
|
backgroundColor: '#f1f2f4',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autocompleteContainerStyle = (isFocused: boolean) => {
|
||||||
|
if (isFocused) {
|
||||||
|
return {
|
||||||
|
borderTopLeftRadius: '24px',
|
||||||
|
borderTopRightRadius: '24px',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
borderRadius: '24px',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ConfirmRecoveryPhrase from './ConfirmRecoveryPhrase'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ConfirmRecoveryPhrase',
|
||||||
|
component: ConfirmRecoveryPhrase,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof ConfirmRecoveryPhrase>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import AutocompleteInput from './AutocompleteInput'
|
||||||
|
import KeyGenerationTitle from '../KeyGenerationTitle'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '../../../../redux/store'
|
||||||
|
|
||||||
|
const ConfirmRecoveryPhrase = () => {
|
||||||
|
const { validWords } = useSelector((state: RootState) => state.keyGeneration)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$3'} style={{ width: '100%', marginTop: '20px' }}>
|
||||||
|
<KeyGenerationTitle />
|
||||||
|
<Text size={27}>Confirm Recovery Phrase</Text>
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||||
|
gap: '20px 40px',
|
||||||
|
width: '72%',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{validWords.map((_, index) => (
|
||||||
|
<AutocompleteInput key={index} index={index} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfirmRecoveryPhrase
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import KeyGeneration from './KeyGeneration'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/KeyGeneration',
|
||||||
|
component: KeyGeneration,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof KeyGeneration>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
isConfirmPhraseStage: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmPhraseStage: Story = {
|
||||||
|
args: {
|
||||||
|
isConfirmPhraseStage: true,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import KeyGenerationHeader from './KeyGenerationHeader/KeyGenerationHeader'
|
||||||
|
import RecoveryMechanism from './RecoveryMechanism/RecoveryMechanism'
|
||||||
|
import KeystoreFiles from './KeystoreFiles'
|
||||||
|
import RecoveryPhrase from './RecoveryPhrase'
|
||||||
|
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../constants'
|
||||||
|
import ConfirmRecoveryPhrase from './ConfirmRecoveryPhrase/ConfirmRecoveryPhrase'
|
||||||
|
|
||||||
|
type KeyGenerationProps = {
|
||||||
|
isConfirmPhraseStage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyGeneration = ({ isConfirmPhraseStage }: KeyGenerationProps) => {
|
||||||
|
const [recoveryMechanism, setRecoveryMechanism] = useState(KEYSTORE_FILES)
|
||||||
|
|
||||||
|
const isKeystoreFiles =
|
||||||
|
recoveryMechanism === KEYSTORE_FILES || recoveryMechanism === BOTH_KEY_AND_RECOVERY
|
||||||
|
|
||||||
|
const isRecoveryPhrase =
|
||||||
|
recoveryMechanism === RECOVERY_PHRASE || recoveryMechanism === BOTH_KEY_AND_RECOVERY
|
||||||
|
|
||||||
|
const handleRecMechanismChange = (value: string) => {
|
||||||
|
setRecoveryMechanism(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$2'} style={{ width: '100%', padding: '16px 32px', alignItems: 'start' }}>
|
||||||
|
{isConfirmPhraseStage && <ConfirmRecoveryPhrase />}
|
||||||
|
{isConfirmPhraseStage === false && (
|
||||||
|
<>
|
||||||
|
<KeyGenerationHeader />
|
||||||
|
<RecoveryMechanism
|
||||||
|
recoveryMechanism={recoveryMechanism}
|
||||||
|
handleRecMechanismChange={handleRecMechanismChange}
|
||||||
|
/>
|
||||||
|
<Stack style={{ margin: '30px 0' }}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
4 Validators
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
{isKeystoreFiles && <KeystoreFiles />}
|
||||||
|
{isRecoveryPhrase && <RecoveryPhrase isKeystoreFiles={isKeystoreFiles} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyGeneration
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import KeyGenerationHeader from './KeyGenerationHeader'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/KeyGenerationHeader',
|
||||||
|
component: KeyGenerationHeader,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof KeyGenerationHeader>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { XStack } from 'tamagui'
|
||||||
|
|
||||||
|
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
|
||||||
|
import KeyGenerationTitle from '../KeyGenerationTitle'
|
||||||
|
|
||||||
|
const KeyGenerationHeader = () => {
|
||||||
|
return (
|
||||||
|
<XStack style={{ width: '100%', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<KeyGenerationTitle />
|
||||||
|
<XStack space={'$2'}>
|
||||||
|
<KeyGenerationSyncCard
|
||||||
|
synced={123.524}
|
||||||
|
total={172.503}
|
||||||
|
title="Execution Sync Status"
|
||||||
|
color="#2a4af5"
|
||||||
|
/>
|
||||||
|
<KeyGenerationSyncCard
|
||||||
|
synced={123.524}
|
||||||
|
total={172.503}
|
||||||
|
title="Consensus Sync Status"
|
||||||
|
color="#ff6161"
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyGenerationHeader
|
|
@ -0,0 +1,78 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import KeyGenerationSyncCard from './KeyGenerationSyncCard'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/KeyGenerationSyncCard',
|
||||||
|
component: KeyGenerationSyncCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof KeyGenerationSyncCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Blue: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 123.524,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Red: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 123.524,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#ff6161',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MaxValue: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 172.503,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OverMaxValue: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 200,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MinValue: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 0,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnderMinValue: Story = {
|
||||||
|
args: {
|
||||||
|
synced: -100,
|
||||||
|
total: 172.503,
|
||||||
|
title: 'Execution Sync Status',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
synced: 123.524,
|
||||||
|
total: 172.503,
|
||||||
|
title: '',
|
||||||
|
color: '#2a4af5',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import { ClearIcon } from '@status-im/icons'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import StandardGauge from '../../../../components/Charts/StandardGauge'
|
||||||
|
import BorderBox from '../../../../components/General/BorderBox'
|
||||||
|
import { formatNumberForGauge } from '../../../../utilities'
|
||||||
|
|
||||||
|
type KeyGenerationSyncCardProps = {
|
||||||
|
synced: number
|
||||||
|
total: number
|
||||||
|
title: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyGenerationSyncCard = ({ synced, total, title, color }: KeyGenerationSyncCardProps) => {
|
||||||
|
return (
|
||||||
|
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
|
||||||
|
<XStack space={'$2'} alignItems="center">
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StandardGauge
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
id: title,
|
||||||
|
label: title,
|
||||||
|
value: synced,
|
||||||
|
color: color,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
label: 'free',
|
||||||
|
value: total - synced || 1,
|
||||||
|
color: '#E7EAEE',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<YStack>
|
||||||
|
<Text size={11} color="#84888e" weight={'semibold'}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
<ClearIcon size={20} color="#A1ABBD" />
|
||||||
|
</XStack>
|
||||||
|
</BorderBox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyGenerationSyncCard
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import KeyGenerationTitle from './KeyGenerationTitle'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/KeyGenerationTitle',
|
||||||
|
component: KeyGenerationTitle,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof KeyGenerationTitle>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
const KeyGenerationTitle = () => {
|
||||||
|
return (
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
Key Generation
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyGenerationTitle
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import KeystoreFiles from './KeystoreFiles'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/KeystoreFiles',
|
||||||
|
component: KeystoreFiles,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof KeystoreFiles>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import { Button, InformationBox, Input, Text } from '@status-im/components'
|
||||||
|
import { ClearIcon, CloseCircleIcon } from '@status-im/icons'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const KeystoreFiles = () => {
|
||||||
|
const [encryptedPassword, setEncryptedPassword] = useState('')
|
||||||
|
const [confirmEncryptedPassword, setConfirmEncryptedPassword] = useState('')
|
||||||
|
|
||||||
|
const generateKeystoreFilesHandler = () => {}
|
||||||
|
|
||||||
|
const changeEncryptedPasswordHandler = (e: any) => {
|
||||||
|
setEncryptedPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeConfirmEncryptedPasswordHandler = (e: any) => {
|
||||||
|
setConfirmEncryptedPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearEncryptedPasswordHandler = () => {
|
||||||
|
setEncryptedPassword('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearConfirmEncryptedPasswordHandler = () => {
|
||||||
|
setConfirmEncryptedPassword('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadKeyFilesHandler = () => {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$4'}>
|
||||||
|
<XStack space={'$2'} style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<YStack space={'$4'} style={{ width: '66%' }}>
|
||||||
|
<YStack space={'$4'}>
|
||||||
|
<Text size={15} color={'#647084'}>
|
||||||
|
Encryption Password
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
placeholder={'******************'}
|
||||||
|
icon={
|
||||||
|
<ClearIcon
|
||||||
|
size={16}
|
||||||
|
color="#A1ABBD"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={clearEncryptedPasswordHandler}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={encryptedPassword}
|
||||||
|
onChange={changeEncryptedPasswordHandler}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
<YStack space={'$2'}>
|
||||||
|
<Text size={15} color={'#647084'}>
|
||||||
|
Confirm Encryption Password
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
placeholder={'******************'}
|
||||||
|
icon={
|
||||||
|
<ClearIcon
|
||||||
|
size={16}
|
||||||
|
color="#A1ABBD"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={clearConfirmEncryptedPasswordHandler}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={confirmEncryptedPassword}
|
||||||
|
onChange={changeConfirmEncryptedPasswordHandler}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
<YStack
|
||||||
|
style={{
|
||||||
|
border: '1px solid #DCE0E5',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
width: '32%',
|
||||||
|
marginTop: '3.4%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={downloadKeyFilesHandler}
|
||||||
|
>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
Download Key Files
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
<Stack style={{ width: 'fit-content' }}>
|
||||||
|
<Button onPress={generateKeystoreFilesHandler}>Generate Key files</Button>
|
||||||
|
</Stack>
|
||||||
|
<InformationBox
|
||||||
|
message="You should see that you have one keystore per validator. This keystore contains your signing key, encrypted with your password. Warning: Do not store keys on multiple (backup) validator clients at once"
|
||||||
|
variant="error"
|
||||||
|
icon={<CloseCircleIcon size={20} />}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeystoreFiles
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import RecoveryMechanism from './RecoveryMechanism'
|
||||||
|
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/RecoveryMechanism',
|
||||||
|
component: RecoveryMechanism,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof RecoveryMechanism>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const KeystoreFiles: Story = {
|
||||||
|
args: {
|
||||||
|
recoveryMechanism: KEYSTORE_FILES,
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecoveryPhrase: Story = {
|
||||||
|
args: {
|
||||||
|
recoveryMechanism: RECOVERY_PHRASE,
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BothKeystoreAndRecovery: Story = {
|
||||||
|
args: {
|
||||||
|
recoveryMechanism: BOTH_KEY_AND_RECOVERY,
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutRecMechanism: Story = {
|
||||||
|
args: {
|
||||||
|
recoveryMechanism: '',
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { XStack, YStack } from 'tamagui'
|
||||||
|
|
||||||
|
import RecoveryMechanismCard from './RecoveryMechanismCard'
|
||||||
|
import { BOTH_KEY_AND_RECOVERY, KEYSTORE_FILES, RECOVERY_PHRASE } from '../../../../constants'
|
||||||
|
|
||||||
|
type RecoveryMechanismProps = {
|
||||||
|
recoveryMechanism: string
|
||||||
|
handleRecMechanismChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [RECOVERY_PHRASE, KEYSTORE_FILES, BOTH_KEY_AND_RECOVERY]
|
||||||
|
|
||||||
|
const RecoveryMechanism = ({
|
||||||
|
recoveryMechanism,
|
||||||
|
handleRecMechanismChange,
|
||||||
|
}: RecoveryMechanismProps) => {
|
||||||
|
return (
|
||||||
|
<YStack style={{ width: '100%' }}>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
Select Recovery Mechanism
|
||||||
|
</Text>
|
||||||
|
<XStack space={'$4'} style={{ justifyContent: 'space-between', marginTop: '40px' }}>
|
||||||
|
{cards.map(value => (
|
||||||
|
<RecoveryMechanismCard
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
recoveryMechanism={recoveryMechanism}
|
||||||
|
handleRecMechanismChange={handleRecMechanismChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecoveryMechanism
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import RecoveryMechanismCard from './RecoveryMechanismCard'
|
||||||
|
import { KEYSTORE_FILES } from '../../../../constants'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/RecoveryMechanismCard',
|
||||||
|
component: RecoveryMechanismCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof RecoveryMechanismCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Selected: Story = {
|
||||||
|
args: {
|
||||||
|
value: KEYSTORE_FILES,
|
||||||
|
recoveryMechanism: KEYSTORE_FILES,
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotSelected: Story = {
|
||||||
|
args: {
|
||||||
|
value: KEYSTORE_FILES,
|
||||||
|
recoveryMechanism: '',
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutValue: Story = {
|
||||||
|
args: {
|
||||||
|
value: '',
|
||||||
|
recoveryMechanism: KEYSTORE_FILES,
|
||||||
|
handleRecMechanismChange: () => {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
type RecoveryMechanismProps = {
|
||||||
|
value: string
|
||||||
|
recoveryMechanism: string
|
||||||
|
handleRecMechanismChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecoveryMechanismCard = ({
|
||||||
|
value,
|
||||||
|
recoveryMechanism,
|
||||||
|
handleRecMechanismChange,
|
||||||
|
}: RecoveryMechanismProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: `1px solid ${recoveryMechanism === value ? '#2A4AF566' : '#DCE0E5'}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: recoveryMechanism === value ? '#f4f6fe' : '#fff',
|
||||||
|
width: '100%',
|
||||||
|
height: '140px',
|
||||||
|
}}
|
||||||
|
onClick={() => handleRecMechanismChange(value)}
|
||||||
|
>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecoveryMechanismCard
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import RecoveryPhrase from './RecoveryPhrase'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/RecoveryPhrase',
|
||||||
|
component: RecoveryPhrase,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof RecoveryPhrase>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
isKeystoreFiles: false,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import { Button, InformationBox, Text } from '@status-im/components'
|
||||||
|
import { CloseCircleIcon } from '@status-im/icons'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type RecoveryPhraseProps = {
|
||||||
|
isKeystoreFiles: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecoveryPhrase = ({ isKeystoreFiles }: RecoveryPhraseProps) => {
|
||||||
|
const [isReveal, setIsReveal] = useState(false)
|
||||||
|
|
||||||
|
const revealHandler = () => {
|
||||||
|
setIsReveal(state => !state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack space={'$4'} style={{ width: '100%', marginTop: isKeystoreFiles ? '20px' : '0px' }}>
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
border: `1px solid #2A4AF566`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '28px 18px',
|
||||||
|
backgroundColor: '#f4f6fe',
|
||||||
|
width: '100%',
|
||||||
|
height: '176px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<YStack space={'$2'} style={{ filter: `blur(${isReveal ? '0px' : '4px'})` }}>
|
||||||
|
<XStack space={'$6'}>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
this is your secret recovery phrase for the validator
|
||||||
|
</Text>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
this is your secret recovery phrase for the validator
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
<XStack space={'$6'}>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
this is your secret recovery phrase for the validator
|
||||||
|
</Text>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
this is your secret recovery phrase for the validator
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</Stack>
|
||||||
|
<Stack style={{ width: 'fit-content', marginBottom: '12px' }}>
|
||||||
|
<Button onPress={revealHandler}>Reveal Recovery Phrase</Button>
|
||||||
|
</Stack>
|
||||||
|
<InformationBox
|
||||||
|
message="Write down and keep your Secret Recovery Phrase in a secure place. Make sure no one is looking at your screen."
|
||||||
|
variant="error"
|
||||||
|
icon={<CloseCircleIcon size={20} />}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecoveryPhrase
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import Overview from './Overview'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/Overview',
|
||||||
|
component: Overview,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof Overview>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Text as TextTam, XStack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import OverviewCard from './OverviewCard'
|
||||||
|
import LinkWithArrow from '../../../components/General/LinkWithArrow'
|
||||||
|
|
||||||
|
const Overview = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<YStack className="layout-left" space={'$5'} style={{ padding: '16px 32px' }}>
|
||||||
|
<TextTam fontSize={27} fontWeight={'600'}>
|
||||||
|
Overview
|
||||||
|
</TextTam>
|
||||||
|
<Text size={27}>
|
||||||
|
Becoming a validator is a big responsibility with important preparation steps. Only start
|
||||||
|
the deposit process when youre 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%' }}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</YStack>
|
||||||
|
<section className="layout-right">
|
||||||
|
<div className="image-container">
|
||||||
|
<img
|
||||||
|
src="./background-images/sync-status-background.png"
|
||||||
|
alt="background"
|
||||||
|
className="background-img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Overview
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import OverviewCard from './OverviewCard'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/OverviewCard',
|
||||||
|
component: OverviewCard,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof OverviewCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const CurrentAPR: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Current APR',
|
||||||
|
value: '4.40%',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TotalETHStaked: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Total ETH Staked',
|
||||||
|
value: '9,451,123',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EstimatedActivationTime: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Estimated Activation Time',
|
||||||
|
value: '32 Days%',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorQueue: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Validator Queue',
|
||||||
|
value: '92603',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
text: '',
|
||||||
|
value: '92603',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutValue: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'Validator Queue',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutData: Story = {
|
||||||
|
args: {
|
||||||
|
text: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
type OverviewCardProps = {
|
||||||
|
text: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const OverviewCard = ({ text, value }: OverviewCardProps) => {
|
||||||
|
return (
|
||||||
|
<YStack
|
||||||
|
style={{
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.15)',
|
||||||
|
width: '46%',
|
||||||
|
padding: '12px 16px',
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
<Text size={27} color="blue" weight={'semibold'}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverviewCard
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ValidatorBoxWrapper from './ValidatorBoxWrapper'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ValidatorBoxWrapper',
|
||||||
|
component: ValidatorBoxWrapper,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof ValidatorBoxWrapper>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'ValidatorBoxWrapper',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Shadow } from '@status-im/components'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
type ValidatorBoxWrapperProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidatorBoxWrapper = ({ children }: ValidatorBoxWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<Shadow
|
||||||
|
variant="$2"
|
||||||
|
style={{
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: 'none',
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
zIndex: 999,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Shadow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatorBoxWrapper
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import ValidatorOnboarding from './ValidatorOnboarding'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Pages/ValidatorOnboarding',
|
||||||
|
component: ValidatorOnboarding,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof ValidatorOnboarding>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Page: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { YStack } from 'tamagui'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import FormStepper from './FormStepper/FormStepper'
|
||||||
|
import Titles from '../../components/General/Titles'
|
||||||
|
import Overview from './Overview/Overview'
|
||||||
|
import KeyGeneration from './KeyGeneration/KeyGeneration'
|
||||||
|
import Activation from './Activation/Activation'
|
||||||
|
import ValidatorBoxWrapper from './ValidatorBoxWrapper/ValidatorBoxWrapper'
|
||||||
|
import ClientSetup from './ClientSetup/ClientSetup'
|
||||||
|
import ConsensusSelection from './ValidatorSetup/ConsensusClient/ConsensusSelection'
|
||||||
|
import Advisories from './Advisories/Advisories'
|
||||||
|
import ValidatorSetup from './ValidatorSetup/ValidatorSetup/ValidatorSetup'
|
||||||
|
import ValidatorSetupInstall from './ValidatorSetup/ValidatorInstalling/ValidatorInstall'
|
||||||
|
import ContinueButton from './ContinueButton'
|
||||||
|
import {
|
||||||
|
setIsCopyPastedPhrase,
|
||||||
|
setValidWords,
|
||||||
|
} from '../../redux/ValidatorOnboarding/KeyGeneration/slice'
|
||||||
|
import { RootState } from '../../redux/store'
|
||||||
|
import './layoutGradient.css'
|
||||||
|
import ActivationValidatorSetup from './ValidatorSetup/ValidatorActivation/ActivationValidatorSetup'
|
||||||
|
import wordlist from 'web-bip39/wordlists/english'
|
||||||
|
|
||||||
|
const ValidatorOnboarding = () => {
|
||||||
|
const [activeStep, setActiveStep] = useState(0)
|
||||||
|
const [isConfirmPhraseStage, setIsConfirmPhraseStage] = useState(false)
|
||||||
|
const [subStepValidatorSetup, setSubStepValidatorSetup] = useState(0)
|
||||||
|
const { isCopyPastedPhrase, words } = useSelector((state: RootState) => state.keyGeneration)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const changeActiveStep = (step: number) => {
|
||||||
|
setActiveStep(step)
|
||||||
|
removeCopyPastePhraseInfoBox()
|
||||||
|
removeConfirmPhraseStage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const continueHandler = () => {
|
||||||
|
if (activeStep === 4 && isConfirmPhraseStage === false) {
|
||||||
|
return setIsConfirmPhraseStage(true)
|
||||||
|
} else if (activeStep === 4 && isConfirmPhraseStage === true) {
|
||||||
|
const newValidWords = words.map(w => wordlist.includes(w))
|
||||||
|
dispatch(setValidWords(newValidWords))
|
||||||
|
|
||||||
|
if (newValidWords.every(w => w === true)) {
|
||||||
|
setActiveStep(activeStep + 1)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (activeStep === 3 && subStepValidatorSetup < 3) {
|
||||||
|
setSubStepValidatorSetup(subStepValidatorSetup + 1)
|
||||||
|
} else if (activeStep < 5) {
|
||||||
|
setActiveStep(activeStep + 1)
|
||||||
|
if (activeStep === 3 && subStepValidatorSetup === 2) {
|
||||||
|
setSubStepValidatorSetup(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCopyPastePhraseInfoBox()
|
||||||
|
removeConfirmPhraseStage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCopyPastePhraseInfoBox = () => {
|
||||||
|
if (isCopyPastedPhrase) {
|
||||||
|
dispatch(setIsCopyPastedPhrase(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeConfirmPhraseStage = () => {
|
||||||
|
if (isConfirmPhraseStage) {
|
||||||
|
setIsConfirmPhraseStage(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gradient-wrapper">
|
||||||
|
<YStack
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '2% 10% 2%',
|
||||||
|
justifyContent: 'start',
|
||||||
|
alignItems: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Titles
|
||||||
|
title="Create Nimbus Validator"
|
||||||
|
titleSize={19}
|
||||||
|
subtitle="Earn Rewards for securing the Ethereum Network"
|
||||||
|
/>
|
||||||
|
<FormStepper activeStep={activeStep} changeActiveStep={changeActiveStep} />
|
||||||
|
<ValidatorBoxWrapper>
|
||||||
|
{activeStep === 0 && <Overview />}
|
||||||
|
{activeStep === 1 && <Advisories />}
|
||||||
|
{activeStep === 2 && <ClientSetup />}
|
||||||
|
|
||||||
|
{activeStep === 3 && subStepValidatorSetup === 0 && <ValidatorSetup />}
|
||||||
|
{activeStep === 3 && subStepValidatorSetup === 1 && <ValidatorSetupInstall />}
|
||||||
|
{activeStep === 3 && subStepValidatorSetup === 2 && <ConsensusSelection />}
|
||||||
|
{activeStep === 3 && subStepValidatorSetup === 3 && <ActivationValidatorSetup />}
|
||||||
|
|
||||||
|
{activeStep === 4 && <KeyGeneration isConfirmPhraseStage={isConfirmPhraseStage} />}
|
||||||
|
{activeStep === 5 && <Activation
|
||||||
|
validatorsValue='4'
|
||||||
|
executionSyncStatus1={{
|
||||||
|
text: "Execution Sync Status",
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: "$blue",
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
}}
|
||||||
|
executionSyncStatus2={{
|
||||||
|
text: "Execution Sync Status",
|
||||||
|
isGaugeIncluded: true,
|
||||||
|
gaugeColor: "$red",
|
||||||
|
gaugeSynced: 123.524,
|
||||||
|
gaugeTotal: 172.503,
|
||||||
|
}}
|
||||||
|
currentAPRValue="4.40%"
|
||||||
|
estimatedActivationTimeValue="32 Days"
|
||||||
|
validatorQueueValue="92603"
|
||||||
|
/>}
|
||||||
|
</ValidatorBoxWrapper>
|
||||||
|
<ContinueButton
|
||||||
|
activeStep={activeStep}
|
||||||
|
continueHandler={continueHandler}
|
||||||
|
isConfirmPhraseStage={isConfirmPhraseStage}
|
||||||
|
subStepValidatorSetup={subStepValidatorSetup}
|
||||||
|
/>
|
||||||
|
</YStack>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidatorOnboarding
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ConsensusClientCard from './ConsensusClientCard'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ConsensusClientCard',
|
||||||
|
component: ConsensusClientCard,
|
||||||
|
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter()],
|
||||||
|
} satisfies Meta<typeof ConsensusClientCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: { name: 'Erigon', icon: '/icons/erigon-circle.png' },
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import Icon from '../../../../components/General/Icon'
|
||||||
|
|
||||||
|
type ConsensusClientCardProps = {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConsensusClientCard = ({ name, icon }: ConsensusClientCardProps) => {
|
||||||
|
return (
|
||||||
|
<YStack
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#2A4AF50D',
|
||||||
|
border: '1px solid #2A4AF5',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '12px 16px',
|
||||||
|
width: '29%',
|
||||||
|
}}
|
||||||
|
space={'$10'}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Icon src={icon} width={100} height={100} />
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConsensusClientCard
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ConsensusGaugeCard from './ConsensusGaugeCard'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ConsensusGaugeCard',
|
||||||
|
component: ConsensusGaugeCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter()],
|
||||||
|
} satisfies Meta<typeof ConsensusGaugeCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'orange', synced: 140000, total: 200000, title: 'Synced Files'
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Stack, XStack, YStack } from 'tamagui'
|
||||||
|
import { ClearIcon } from '@status-im/icons'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
|
||||||
|
import StandardGauge from '../../../../components/Charts/StandardGauge'
|
||||||
|
import BorderBox from '../../../../components/General/BorderBox'
|
||||||
|
import { formatNumberForGauge } from '../../../../utilities'
|
||||||
|
|
||||||
|
type ConsensusGaugeCardProps = {
|
||||||
|
synced: number
|
||||||
|
total: number
|
||||||
|
title: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConsensusGaugeCard = ({ synced, total, title, color }: ConsensusGaugeCardProps) => {
|
||||||
|
return (
|
||||||
|
<BorderBox style={{ borderRadius: '10.1px', borderWidth: '0.5px' }}>
|
||||||
|
<XStack space={'$2'} alignItems="center">
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StandardGauge
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
id: title,
|
||||||
|
label: title,
|
||||||
|
value: synced,
|
||||||
|
color: color,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'free',
|
||||||
|
label: 'free',
|
||||||
|
value: total - synced || 1,
|
||||||
|
color: '#E7EAEE',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<YStack>
|
||||||
|
<Text size={11} color="#84888e" weight={'semibold'}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
{formatNumberForGauge(synced)} / {formatNumberForGauge(total)}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
<ClearIcon size={20} color="#A1ABBD" />
|
||||||
|
</XStack>
|
||||||
|
</BorderBox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConsensusGaugeCard
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
import ConsensusSelection from './ConsensusSelection'
|
||||||
|
import { StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ConsensusSelection',
|
||||||
|
component: ConsensusSelection,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
}
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { XStack, Stack, Text as TextTam, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import PairedDeviceCard from './PairedDeviceCard'
|
||||||
|
import ConsensusGaugeCard from './ConsensusGaugeCard'
|
||||||
|
import ConsensusClientCard from './ConsensusClientCard'
|
||||||
|
import LinkWithArrow from '../../../../components/General/LinkWithArrow'
|
||||||
|
import { RootState } from '../../../../redux/store'
|
||||||
|
|
||||||
|
const clientIcons = {
|
||||||
|
Nethermind: '/icons/nethermind-circle.png',
|
||||||
|
Besu: '/icons/hyperledger-besu-circle.png',
|
||||||
|
Geth: '/icons/gethereum-mascot-circle.png',
|
||||||
|
Erigon: '/icons/erigon-circle.png',
|
||||||
|
Nimbus: '/icons/NimbusDisabled.svg',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConsensusSelection = () => {
|
||||||
|
const selectedClient = useSelector((state: RootState) => state.execClient.selectedClient) as
|
||||||
|
| 'Nethermind'
|
||||||
|
| 'Besu'
|
||||||
|
| 'Geth'
|
||||||
|
| 'Erigon'
|
||||||
|
| 'Nimbus'
|
||||||
|
|
||||||
|
const clients = [
|
||||||
|
{
|
||||||
|
name: selectedClient,
|
||||||
|
icon: clientIcons[selectedClient],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack style={{ width: '100%', padding: '32px' }}>
|
||||||
|
<XStack justifyContent={'space-between'}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
Validator Setup
|
||||||
|
</Text>
|
||||||
|
<XStack space={'$2'}>
|
||||||
|
<ConsensusGaugeCard
|
||||||
|
color="blue"
|
||||||
|
synced={134879}
|
||||||
|
title="Execution Sync Status"
|
||||||
|
total={150000}
|
||||||
|
/>
|
||||||
|
<PairedDeviceCard isVisibleState={true} />
|
||||||
|
</XStack>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<YStack>
|
||||||
|
<Stack style={{ marginBottom: '4px' }}>
|
||||||
|
<Text size={13} color="#647084">
|
||||||
|
Consensus Client Detection
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text size={15} weight={'regular'}>
|
||||||
|
No existing execution client installations have been detected on paired device.
|
||||||
|
</Text>
|
||||||
|
<Text size={13} color="#828282">
|
||||||
|
If you believe this to be incorrect please test your pairing to the correct device and try
|
||||||
|
again.
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
<TextTam fontSize={27} style={{ margin: '5px', marginLeft: 0, marginTop: '50px' }}>
|
||||||
|
Install Consensus client
|
||||||
|
</TextTam>
|
||||||
|
|
||||||
|
<XStack space={'$8'}>
|
||||||
|
<ConsensusClientCard name={clients[0].name} icon={clients[0].icon} />
|
||||||
|
<YStack width={'67%'} space={'$4'}>
|
||||||
|
<Text size={27}>The resource efficient Ethereum Clients.</Text>
|
||||||
|
<Text size={15}>
|
||||||
|
{selectedClient} is a client implementation for both execution and consensus layers that
|
||||||
|
strives to be as lightweight as possible in terms of resources used. This allows it to
|
||||||
|
perform well on embedded systems, resource-restricted devices -- including Raspberry Pis
|
||||||
|
-- and multi-purpose servers.
|
||||||
|
</Text>
|
||||||
|
<Text size={19} weight={'semibold'}>
|
||||||
|
<LinkWithArrow
|
||||||
|
textColor="black"
|
||||||
|
text="Nimbus Documentation"
|
||||||
|
arrowRight={true}
|
||||||
|
to="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConsensusSelection
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import PairedDeviceCard from './PairedDeviceCard'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/PairedDeviceCard',
|
||||||
|
component: PairedDeviceCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter],
|
||||||
|
} satisfies Meta<typeof PairedDeviceCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
isVisibleState: true,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { XStack, YStack } from 'tamagui'
|
||||||
|
import { ClearIcon } from '@status-im/icons'
|
||||||
|
import { Avatar, Text } from '@status-im/components'
|
||||||
|
|
||||||
|
type PairedDeviceCardProps = {
|
||||||
|
isVisibleState: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const PairedDeviceCard = ({ isVisibleState }: PairedDeviceCardProps) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(isVisibleState)
|
||||||
|
}, [isVisibleState])
|
||||||
|
|
||||||
|
if (!isVisible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XStack
|
||||||
|
space={'$7'}
|
||||||
|
style={{
|
||||||
|
padding: '2px 6px',
|
||||||
|
border: '1px solid #DCE0E5',
|
||||||
|
borderRadius: '15px',
|
||||||
|
}}
|
||||||
|
alignItems={'center'}
|
||||||
|
>
|
||||||
|
<XStack space={'$3'} alignItems={'center'}>
|
||||||
|
<Avatar backgroundColor="pink" size={32} type="user" name="RP" />
|
||||||
|
<YStack>
|
||||||
|
<Text size={13} color="#647084">
|
||||||
|
Paired Device
|
||||||
|
</Text>
|
||||||
|
<Text size={15} weight={'semibold'}>
|
||||||
|
Stake & Chips
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
<ClearIcon size={20} color="#A1ABBD" cursor={'pointer'} onClick={() => setIsVisible(false)} />
|
||||||
|
</XStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PairedDeviceCard
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import ActivationValidatorSetup from './ActivationValidatorSetup'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/ActivationValidatorSetup',
|
||||||
|
component: ActivationValidatorSetup,
|
||||||
|
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter()],
|
||||||
|
} satisfies Meta<typeof ActivationValidatorSetup>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: { },
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { XStack, Stack, YStack } from 'tamagui'
|
||||||
|
import { Text } from '@status-im/components'
|
||||||
|
import Confetti from 'react-confetti'
|
||||||
|
|
||||||
|
import ActivationCard from '../../Activation/ActivationCard'
|
||||||
|
|
||||||
|
const ActivationValidatorSetup = () => {
|
||||||
|
const [showConfetti, setShowConfetti] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowConfetti(false)
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={styles.confettiContainer} width={'100%'}>
|
||||||
|
{showConfetti && <Confetti style={styles.confettiCanvas} />}
|
||||||
|
<YStack style={{ padding: '16px 32px' }}>
|
||||||
|
<YStack space={'$3'}>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
Activation
|
||||||
|
</Text>
|
||||||
|
<Stack>
|
||||||
|
<Text size={27}>
|
||||||
|
Congratulations! You have successfully setup your Execution and Consensus clients and
|
||||||
|
are currently syncing your nodes. You need to be sufficiently synced prior to setting
|
||||||
|
up your validators and making a deposit.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<YStack space={'$3'} marginTop={'10px'} width={'33%'}>
|
||||||
|
<XStack width={'151%'} space={'$3'}>
|
||||||
|
<ActivationCard
|
||||||
|
text="Execution Sync Status"
|
||||||
|
isGaugeIncluded={true}
|
||||||
|
gaugeColor={'#2a4af5'}
|
||||||
|
gaugeSynced={123.524}
|
||||||
|
gaugeTotal={172.503}
|
||||||
|
/>
|
||||||
|
<ActivationCard
|
||||||
|
text="Execution Sync Status"
|
||||||
|
isGaugeIncluded={true}
|
||||||
|
gaugeColor={'#EB5757'}
|
||||||
|
gaugeSynced={123.524}
|
||||||
|
gaugeTotal={172.503}
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
<XStack space={'$3'}>
|
||||||
|
<ActivationCard text="Validator Queue" value="92603" />
|
||||||
|
<ActivationCard text="Estimated Activation Time" value="32 Days" />
|
||||||
|
<ActivationCard text="Current APR" value="4.40%" />
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActivationValidatorSetup
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
confettiContainer: {
|
||||||
|
position: 'relative' as const,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'fit-content',
|
||||||
|
},
|
||||||
|
confettiCanvas: {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 0,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import OsCard from './OsCard'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/OsCard',
|
||||||
|
component: OsCard,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof OsCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
icon: '/icons/MAC.png',
|
||||||
|
name: 'Mac',
|
||||||
|
isSelected: true,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Stack, YStack } 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) => {
|
||||||
|
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: '33%',
|
||||||
|
}}
|
||||||
|
space={'$12'}
|
||||||
|
onPress={onClick}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text size={27} weight={'semibold'}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Icon src={icon} width={100} height={100} />
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OsCard
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
import SyntaxHighlighter from './SyntaxHighlighter'
|
||||||
|
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ValidatorOnboarding/SyntaxHighlighter',
|
||||||
|
component: SyntaxHighlighter,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
decorators: [withRouter()],
|
||||||
|
} satisfies Meta<typeof SyntaxHighlighter>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: { rows: ['yarn', 'yarn build', 'yarn dev', 'house'] },
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
|
import { solarizedlight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
|
||||||
|
type SyntaxHighlighterBoxProps = {
|
||||||
|
rows: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const customStyle = {
|
||||||
|
...solarizedlight,
|
||||||
|
'pre[class*="language-"]': {
|
||||||
|
...solarizedlight['pre[class*="language-"]'],
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
backgroundColor: 'white',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SyntaxHighlighterBox = ({ rows }: SyntaxHighlighterBoxProps) => {
|
||||||
|
return (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="bash"
|
||||||
|
showLineNumbers={true}
|
||||||
|
lineNumberStyle={{ backgroundColor: '#E7EAEE' }}
|
||||||
|
lineNumberContainerStyle={{ color: 'black' }}
|
||||||
|
customStyle={customStyle}
|
||||||
|
>
|
||||||
|
{`${rows.join('\n')}`}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SyntaxHighlighterBox
|