1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Atomic components stories (#1422)

This commit is contained in:
claudiaHash 2022-05-23 14:29:37 +03:00 committed by GitHub
parent 9027fc1307
commit 6d2dea8c9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 10240 additions and 4715 deletions

View File

@ -127,4 +127,5 @@ jobs:
restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-
- run: npm ci
- run: npm run pregenerate
- run: npm run storybook:build

View File

@ -0,0 +1,13 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
})

View File

@ -1 +1,2 @@
import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia'

View File

@ -1,5 +1,5 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const webpack = require('webpack')
module.exports = {
core: { builder: 'webpack5' },
stories: ['../src/**/*.stories.tsx'],
@ -47,6 +47,12 @@ module.exports = {
})
config.resolve.fallback = fallback
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
})
])
return config
}
}

View File

@ -13,7 +13,7 @@ export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
date: /date$/
}
}
}

13808
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,8 @@
"deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "start-storybook -p 6006 --quiet",
"storybook:build": "build-storybook"
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook"
},
"dependencies": {
"@coingecko/cryptoformat": "^0.4.4",
@ -58,7 +58,7 @@
"react-modal": "^3.15.1",
"react-paginate": "^8.1.3",
"react-spring": "^9.4.5",
"react-tabs": "^3.2.3",
"react-tabs": "^5.1.0",
"react-toastify": "^8.2.0",
"remark": "^13.0.0",
"remark-gfm": "^1.0.0",
@ -73,41 +73,40 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-storyshots": "^6.4.22",
"@storybook/builder-webpack5": "^6.4.22",
"@storybook/manager-webpack5": "^6.4.22",
"@storybook/react": "^6.4.22",
"@storybook/addon-essentials": "^6.5.4",
"@storybook/addon-storyshots": "^6.5.4",
"@storybook/builder-webpack5": "^6.5.4",
"@storybook/manager-webpack5": "^6.5.4",
"@storybook/react": "^6.5.4",
"@storybook/testing-library": "^0.0.11",
"@storybook/testing-react": "^1.2.4",
"@storybook/testing-react": "^1.3.0",
"@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0",
"@types/chart.js": "^2.9.37",
"@types/d3": "^7.1.0",
"@types/js-cookie": "^3.0.1",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6",
"@types/node": "^17.0.13",
"@types/node": "^17.0.35",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@types/react-dom": "^18.0.4",
"@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1",
"@types/react-tabs": "^2.3.4",
"@types/remove-markdown": "^0.3.1",
"@types/yup": "^0.29.13",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"apollo": "^2.33.9",
"eslint": "^8.15.0",
"cross-env": "^7.0.3",
"eslint": "^8.16.0",
"eslint-config-oceanprotocol": "^2.0.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.1",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-testing-library": "^5.4.0",
"eslint-plugin-testing-library": "^5.5.0",
"file-loader": "^6.2.0",
"https-browserify": "^1.0.0",
"husky": "^8.0.1",

View File

@ -58,11 +58,11 @@
}
.radio {
composes: radio from '@shared/FormInput/InputElement.module.css';
composes: radio from '@shared/FormInput/InputRadio.module.css';
}
.checkbox {
composes: checkbox from '@shared/FormInput/InputElement.module.css';
composes: checkbox from '@shared/FormInput/InputRadio.module.css';
}
.title {

View File

@ -76,10 +76,10 @@ export default function AssetSelection({
<div className={styles.row} key={asset.did}>
<input
id={slugify(asset.did)}
type={multiple ? 'checkbox' : 'radio'}
className={styleClassesInput}
defaultChecked={asset.checked}
{...props}
defaultChecked={asset.checked}
type={multiple ? 'checkbox' : 'radio'}
disabled={disabled}
value={asset.did}
/>

View File

@ -45,11 +45,11 @@ export default function BoxSelection({
<div key={option.name}>
<input
id={option.name}
type="radio"
className={styleClassesInput}
defaultChecked={option.checked}
onChange={(event) => handleChange(event)}
{...props}
type="radio"
className={styleClassesInput}
disabled={disabled}
value={option.value ? option.value : option.name}
name={name}

View File

@ -81,92 +81,12 @@
font-family: var(--font-family-base);
}
.radioGroup {
margin-top: calc(var(--spacer) / 2);
}
.radioWrap {
position: relative;
}
.radioLabel {
margin: 0;
padding: 0;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
padding-left: 0.5rem;
}
.algorithmLabel {
display: grid;
gap: var(--spacer);
grid-template-columns: 2fr 1fr;
}
.radio,
.checkbox {
composes: input;
position: relative;
padding: 0;
width: 18px;
height: 18px;
min-height: 0;
display: inline-block;
vertical-align: middle;
margin-top: -2px;
}
.radio::after,
.checkbox::after {
content: '';
display: block;
left: 0;
top: 0;
position: absolute;
opacity: 0;
transition: transform 0.3s ease-out, opacity 0.2s;
}
.radio:checked,
.checkbox:checked {
border-color: var(--color-primary);
background: var(--color-primary);
}
.radio:focus,
.checkbox:focus {
box-shadow: 0 0 0 var(--color-primary);
}
.radio:checked::after,
.checkbox:checked::after {
opacity: 1;
}
.radio,
.radio::after {
border-radius: 50%;
}
.radio::after {
width: 8px;
height: 8px;
top: 4px;
left: 4px;
background: var(--brand-white);
}
.checkbox::after {
width: 6px;
height: 9px;
border: 2px solid var(--brand-white);
border-top: 0;
border-left: 0;
left: 5px;
top: 2px;
transform: rotate(40deg);
}
.prefixGroup,
.postfixGroup {
display: inline-flex;

View File

@ -1,5 +1,4 @@
import React, { ReactElement } from 'react'
import slugify from 'slugify'
import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../FormFields/FilesInput'
@ -11,15 +10,20 @@ import AssetSelection, {
AssetSelectionAsset
} from '../FormFields/AssetSelection'
import Nft from '../FormFields/Nft'
import InputRadio from './InputRadio'
const cx = classNames.bind(styles)
const DefaultInput = ({
size,
className,
// We filter out all props which are not allowed
// to be passed to HTML input so these stay unused.
/* eslint-disable @typescript-eslint/no-unused-vars */
prefix,
postfix,
additionalComponent,
/* eslint-enable @typescript-eslint/no-unused-vars */
...props
}: InputProps) => (
<input
@ -30,27 +34,28 @@ const DefaultInput = ({
)
export default function InputElement({
type,
options,
sortOptions,
name,
prefix,
postfix,
size,
field,
label,
multiple,
disabled,
// We filter out all props which are not allowed
// to be passed to HTML input so these stay unused.
/* eslint-disable @typescript-eslint/no-unused-vars */
label,
help,
prominentHelp,
form,
additionalComponent,
disclaimer,
disclaimerValues,
/* eslint-enable @typescript-eslint/no-unused-vars */
...props
}: InputProps): ReactElement {
const styleClasses = cx({ select: true, [size]: size })
switch (type) {
switch (props.type) {
case 'select': {
const sortedOptions =
!sortOptions && sortOptions === false
@ -60,10 +65,9 @@ export default function InputElement({
)
return (
<select
id={name}
id={props.name}
className={styleClasses}
{...props}
disabled={disabled}
multiple={multiple}
>
{field !== undefined && field.value === '' && <option value="" />}
@ -77,39 +81,12 @@ export default function InputElement({
)
}
case 'textarea':
return (
<textarea
name={name}
id={name}
className={styles.textarea}
{...props}
/>
)
return <textarea id={props.name} className={styles.textarea} {...props} />
case 'radio':
case 'checkbox':
return (
<div className={styles.radioGroup}>
{options &&
(options as string[]).map((option: string, index: number) => (
<div className={styles.radioWrap} key={index}>
<input
className={styles[type]}
id={slugify(option)}
type={type}
name={name}
defaultChecked={props.defaultChecked}
{...props}
/>
<label
className={cx({ [styles.radioLabel]: true, [size]: size })}
htmlFor={slugify(option)}
>
{option}
</label>
</div>
))}
</div>
)
return <InputRadio options={options} inputSize={size} {...props} />
case 'assetSelection':
return (
<AssetSelection
@ -118,28 +95,27 @@ export default function InputElement({
{...props}
/>
)
case 'assetSelectionMultiple':
return (
<AssetSelection
assets={options as unknown as AssetSelectionAsset[]}
multiple
disabled={disabled}
{...field}
{...props}
/>
)
case 'files':
return <FilesInput name={name} {...field} {...props} />
return <FilesInput {...field} {...props} />
case 'providerUrl':
return <CustomProvider name={name} {...field} {...props} />
return <CustomProvider {...field} {...props} />
case 'nft':
return <Nft name={name} {...field} {...props} />
return <Nft {...field} {...props} />
case 'datatoken':
return <Datatoken name={name} {...field} {...props} />
return <Datatoken {...field} {...props} />
case 'boxSelection':
return (
<BoxSelection
name={name}
options={options as unknown as BoxSelectionOption[]}
{...field}
{...props}
@ -152,10 +128,8 @@ export default function InputElement({
<div className={cx({ prefix: true, [size]: size })}>{prefix}</div>
)}
<DefaultInput
name={name}
type={type || 'text'}
type={props.type || 'text'}
size={size}
disabled={disabled}
{...field}
{...props}
/>
@ -165,10 +139,8 @@ export default function InputElement({
</div>
) : (
<DefaultInput
name={name}
type={type || 'text'}
type={props.type || 'text'}
size={size}
disabled={disabled}
{...field}
{...props}
/>

View File

@ -0,0 +1,79 @@
.radioGroup {
margin-top: calc(var(--spacer) / 2);
}
.radioWrap {
position: relative;
}
.radioLabel {
margin: 0;
padding: 0;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
padding-left: 0.5rem;
}
.radio,
.checkbox {
composes: input from './InputElement.module.css';
position: relative;
padding: 0;
width: 18px;
height: 18px;
min-height: 0;
display: inline-block;
vertical-align: middle;
margin-top: -2px;
}
.radio:focus,
.checkbox:focus {
box-shadow: 0 0 0 var(--color-primary);
}
.radio::after,
.checkbox::after {
content: '';
display: block;
left: 0;
top: 0;
position: absolute;
opacity: 0;
transition: transform 0.3s ease-out, opacity 0.2s;
}
.radio,
.radio::after {
border-radius: 50%;
}
.radio::after {
width: 8px;
height: 8px;
top: 4px;
left: 4px;
background: var(--brand-white);
}
.checkbox::after {
width: 6px;
height: 9px;
border: 2px solid var(--brand-white);
border-top: 0;
border-left: 0;
left: 5px;
top: 2px;
transform: rotate(40deg);
}
.radio:checked,
.checkbox:checked {
border-color: var(--color-primary);
background: var(--color-primary);
}
.radio:checked::after,
.checkbox:checked::after {
opacity: 1;
}

View File

@ -0,0 +1,41 @@
import React, { InputHTMLAttributes, ReactElement } from 'react'
import slugify from 'slugify'
import classNames from 'classnames/bind'
import styles from './InputRadio.module.css'
const cx = classNames.bind(styles)
interface InputRadioProps extends InputHTMLAttributes<HTMLInputElement> {
options: string[]
inputSize?: string
}
export default function InputRadio({
options,
inputSize,
...props
}: InputRadioProps): ReactElement {
return (
<div className={styles.radioGroup}>
{options &&
(options as string[]).map((option: string, index: number) => (
<div className={styles.radioWrap} key={index}>
<input
{...props}
className={styles[props.type]}
id={slugify(option)}
/>
<label
className={cx({
[styles.radioLabel]: true,
[inputSize]: inputSize
})}
htmlFor={slugify(option)}
>
{option}
</label>
</div>
))}
</div>
)
}

View File

@ -264,6 +264,7 @@ export default function PoolTransactions({
minimal ? transactions?.length >= 4 : transactions?.length >= 9
}
paginationPerPage={minimal ? 5 : 10}
emptyMessage={chainIds.length === 0 ? 'No network selected' : null}
/>
) : (
<div>Please connect your Web3 wallet.</div>

View File

@ -0,0 +1,20 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Badge, { BadgeProps } from '@shared/atoms/Badge'
export default {
title: 'Component/@shared/atoms/Badge',
component: Badge
} as ComponentMeta<typeof Badge>
const Template: ComponentStory<typeof Badge> = (args) => <Badge {...args} />
interface Props {
args: BadgeProps
}
export const Default: Props = Template.bind({})
Default.args = {
label: 'Badge label'
}

View File

@ -1,16 +1,15 @@
import React, { ReactElement } from 'react'
import styles from './Badge.module.css'
import styles from './index.module.css'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
export default function Badge({
label,
className
}: {
export interface BadgeProps {
label: string
className?: string
}): ReactElement {
}
export default function Badge({ label, className }: BadgeProps): ReactElement {
const styleClasses = cx({
badge: true,
[className]: className

View File

@ -0,0 +1,22 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Blockies, { BlockiesProps } from '@shared/atoms/Blockies'
export default {
title: 'Component/@shared/atoms/Blockies',
component: Blockies
} as ComponentMeta<typeof Blockies>
const Template: ComponentStory<typeof Blockies> = (args) => (
<Blockies {...args} />
)
interface Props {
args: BlockiesProps
}
export const Default: Props = Template.bind({})
Default.args = {
accountId: '0x1xxxxxxxxxx3Exxxxxx7xxxxxxxxxxxxF1fd'
}

View File

@ -1,14 +1,16 @@
import { toDataUrl } from 'myetherwallet-blockies'
import React, { ReactElement } from 'react'
import styles from './Blockies.module.css'
import styles from './index.module.css'
export interface BlockiesProps {
accountId: string
className?: string
}
export default function Blockies({
accountId,
className
}: {
accountId: string
className?: string
}): ReactElement {
}: BlockiesProps): ReactElement {
if (!accountId) return null
const blockies = toDataUrl(accountId)

View File

@ -0,0 +1,43 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Container, { ContainerProps } from '@shared/atoms/Container'
export default {
title: 'Component/@shared/atoms/Container',
component: Container
} as ComponentMeta<typeof Container>
const Template: ComponentStory<typeof Container> = (args) => (
<Container {...args} />
)
interface Props {
args: ContainerProps
}
export const Default: Props = Template.bind({})
Default.args = {
children: (
<>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam
facilisis molestie. Integer eget congue turpis, in pharetra lectus. Sed
urna dolor, porttitor luctus mauris eget, lacinia consectetur eros. Duis
consequat, turpis et porttitor cursus, ante lacus placerat arcu, vel
pellentesque enim orci ac sem.
</>
)
}
export const Narrow: Props = Template.bind({})
Narrow.args = {
children: (
<>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam
facilisis molestie. Integer eget congue turpis, in pharetra lectus. Sed
urna dolor, porttitor luctus mauris eget, lacinia consectetur eros. Duis
consequat, turpis et porttitor cursus, ante lacus placerat arcu, vel
pellentesque enim orci ac sem.
</>
),
narrow: true
}

View File

@ -1,18 +1,20 @@
import React, { ReactElement, ReactNode } from 'react'
import classNames from 'classnames/bind'
import styles from './Container.module.css'
import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface ContainerProps {
children: ReactNode
narrow?: boolean
className?: string
}
export default function Container({
children,
narrow,
className
}: {
children: ReactNode
narrow?: boolean
className?: string
}): ReactElement {
}: ContainerProps): ReactElement {
const styleClasses = cx({
container: true,
narrow,

View File

@ -0,0 +1,15 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Copy from '@shared/atoms/Copy'
export default {
title: 'Component/@shared/atoms/Copy',
component: Copy
} as ComponentMeta<typeof Copy>
const Template: ComponentStory<typeof Copy> = (args) => <Copy {...args} />
export const Default = Template.bind({})
Default.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.'
}

View File

@ -0,0 +1,27 @@
import React from 'react'
import { render, act, screen, fireEvent } from '@testing-library/react'
import { Default } from './index.stories'
jest.useFakeTimers()
describe('Copy', () => {
test('should change class on click', () => {
render(<Default {...Default.args} />)
const element = screen.getByTitle('Copy to clipboard')
fireEvent.click(element)
expect(element).toHaveClass('copied')
})
test('should remove class after timer end', () => {
render(<Default {...Default.args} />)
const element = screen.getByTitle('Copy to clipboard')
fireEvent.click(element)
act(() => {
jest.advanceTimersToNextTimer()
})
expect(element).not.toHaveClass('copied')
})
})

View File

@ -1,9 +1,13 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './Copy.module.css'
import styles from './index.module.css'
import IconCopy from '@images/copy.svg'
import Clipboard from 'react-clipboard.js'
export default function Copy({ text }: { text: string }): ReactElement {
export interface CopyProps {
text: string
}
export default function Copy({ text }: CopyProps): ReactElement {
const [isCopied, setIsCopied] = useState(false)
// Clear copy success style after 5 sec.

View File

@ -0,0 +1,45 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { ListItem } from '@shared/atoms/Lists'
export default {
title: 'Component/@shared/atoms/Lists',
component: ListItem
} as ComponentMeta<typeof ListItem>
const Template: ComponentStory<typeof ListItem> = (args) => (
<ListItem {...args} />
)
const items = [
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit',
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit'
]
export const Unordered = Template.bind({})
Unordered.decorators = [
() => (
<ul>
{items.map((item, key) => (
<Template key={key}>{item}</Template>
))}
</ul>
)
]
export const Ordered = Template.bind({})
Ordered.decorators = [
() => (
<ol>
{items.map((item, key) => (
<Template ol key={key}>
{item}
</Template>
))}
</ol>
)
]

View File

@ -1,13 +1,12 @@
import React, { ReactElement, ReactNode } from 'react'
import styles from './Lists.module.css'
import styles from './index.module.css'
export function ListItem({
children,
ol
}: {
children: ReactNode
export interface ListItemProps {
children?: ReactNode
ol?: boolean
}): ReactElement {
}
export function ListItem({ children, ol }: ListItemProps): ReactElement {
const classes = ol
? `${styles.item} ${styles.olItem}`
: `${styles.item} ${styles.ulItem}`

View File

@ -0,0 +1,23 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Loader, { LoaderProps } from '@shared/atoms/Loader'
export default {
title: 'Component/@shared/atoms/Loader',
component: Loader
} as ComponentMeta<typeof Loader>
const Template: ComponentStory<typeof Loader> = (args) => <Loader {...args} />
interface Props {
args: LoaderProps
}
export const Default: Props = Template.bind({})
Default.args = {}
export const WithMessage: Props = Template.bind({})
WithMessage.args = {
message: 'Loading...'
}

View File

@ -1,11 +1,11 @@
import React, { ReactElement } from 'react'
import styles from './Loader.module.css'
import styles from './index.module.css'
export default function Loader({
message
}: {
export interface LoaderProps {
message?: string
}): ReactElement {
}
export default function Loader({ message }: LoaderProps): ReactElement {
return (
<div className={styles.loaderWrap}>
<span className={styles.loader} />

View File

@ -14,7 +14,10 @@ interface Props {
args: LogoProps
}
export const Primary: Props = Template.bind({})
Primary.args = {
export const Default: Props = Template.bind({})
Default.args = {}
export const WithoutWordmark: Props = Template.bind({})
WithoutWordmark.args = {
noWordmark: true
}

View File

@ -14,7 +14,7 @@
}
.modal {
composes: box from './Box.module.css';
composes: box from '../Box.module.css';
padding: var(--spacer);
margin: var(--spacer) auto;
max-width: var(--break-point--small);

View File

@ -0,0 +1,32 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Button from '@shared/atoms/Button'
import Modal, { ModalProps } from '@shared/atoms/Modal'
import { useArgs } from '@storybook/client-api'
export default {
title: 'Component/@shared/atoms/Modal',
component: Modal
} as ComponentMeta<typeof Modal>
const Template: ComponentStory<typeof Modal> = (args: ModalProps) => {
const [{ isOpen }, updateArgs] = useArgs()
const handleClose = () => updateArgs({ isOpen: !isOpen })
return (
<>
<Button style="primary" onClick={() => updateArgs({ isOpen: !isOpen })}>
Open Modal
</Button>
<Modal {...args} onToggleModal={handleClose}>
<a>This is a modal</a>
</Modal>
</>
)
}
interface Props {
args: ModalProps
}
export const Default: Props = Template.bind({})

View File

@ -1,6 +1,6 @@
import React, { ReactElement, ReactNode } from 'react'
import ReactModal from 'react-modal'
import styles from './Modal.module.css'
import styles from './index.module.css'
if (process.env.NODE_ENV !== 'test') ReactModal.setAppElement('#__next')

View File

@ -0,0 +1,28 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Status, { StatusProps } from '@shared/atoms/Status'
export default {
title: 'Component/@shared/atoms/Status',
component: Status
} as ComponentMeta<typeof Status>
const Template: ComponentStory<typeof Status> = (args) => <Status {...args} />
interface Props {
args: StatusProps
}
export const Default: Props = Template.bind({})
Default.args = {}
export const Warning: Props = Template.bind({})
Warning.args = {
state: 'warning'
}
export const Error: Props = Template.bind({})
Error.args = {
state: 'error'
}

View File

@ -1,16 +1,18 @@
import React, { ReactElement } from 'react'
import classNames from 'classnames/bind'
import styles from './Status.module.css'
import styles from './index.module.css'
export interface StatusProps {
state?: string
className?: string
}
const cx = classNames.bind(styles)
export default function Status({
state,
className
}: {
state?: string
className?: string
}): ReactElement {
}: StatusProps): ReactElement {
const styleClasses = cx({
status: true,
warning: state === 'warning',

View File

@ -0,0 +1,82 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Table, { TableProps } from '@shared/atoms/Table'
export default {
title: 'Component/@shared/atoms/Table',
component: Table
} as ComponentMeta<typeof Table>
const Template: ComponentStory<typeof Table> = (args) => <Table {...args} />
interface Props {
args: TableProps
}
const columns = [
{
name: 'Name',
selector: (row: any) => row.name,
maxWidth: '45rem',
grow: 1
},
{
name: 'Symbol',
selector: (row: any) => row.symbol,
maxWidth: '10rem'
},
{
name: 'Price',
selector: (row: any) => row.price,
right: true
}
]
const data = [
{
name: 'Title asset',
symbol: 'DATA-70',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
},
{
name: 'Title asset',
symbol: 'DATA-72',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
}
]
export const WithData: Props = Template.bind({})
WithData.args = {
columns,
data
}
export const WithPagination: Props = Template.bind({})
WithPagination.args = {
columns,
data: data.flatMap((i) => [i, i, i])
}
export const Loading: Props = Template.bind({})
Loading.args = {
isLoading: true,
columns: [],
data: []
}
export const Empty: Props = Template.bind({})
Empty.args = {
emptyMessage: 'I am empty',
columns: [],
data: []
}

View File

@ -1,11 +1,10 @@
import React, { ReactElement, ReactNode } from 'react'
import DataTable, { IDataTableProps } from 'react-data-table-component'
import Loader from './Loader'
import Loader from '../Loader'
import Pagination from '@shared/Pagination'
import styles from './Table.module.css'
import { useUserPreferences } from '@context/UserPreferences'
import styles from './index.module.css'
interface TableProps extends IDataTableProps {
export interface TableProps extends IDataTableProps {
isLoading?: boolean
emptyMessage?: string
sortField?: string
@ -14,14 +13,7 @@ interface TableProps extends IDataTableProps {
}
function Empty({ message }: { message?: string }): ReactElement {
const { chainIds } = useUserPreferences()
return (
<div className={styles.empty}>
{chainIds.length === 0
? 'No network selected'
: message || 'No results found'}
</div>
)
return <div className={styles.empty}>{message || 'No results found'}</div>
}
export default function Table({

View File

@ -8,11 +8,10 @@
.tab {
display: inline-block;
padding: calc(var(--spacer) / 12) var(--spacer);
padding: calc(var(--spacer) / 8) var(--spacer);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
text-transform: uppercase;
cursor: pointer;
color: var(--color-secondary);
background-color: var(--background-body);
border: 1px solid var(--border-color);
@ -20,6 +19,11 @@
min-width: 90px;
}
.tab,
.tab label {
cursor: pointer;
}
.tab:first-child {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
@ -53,3 +57,7 @@
padding: var(--spacer);
}
}
.radio {
composes: radio from '../../FormInput/InputRadio.module.css';
}

View File

@ -0,0 +1,52 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Tabs, { TabsProps } from '@shared/atoms/Tabs'
export default {
title: 'Component/@shared/atoms/Tabs',
component: Tabs
} as ComponentMeta<typeof Tabs>
const Template: ComponentStory<typeof Tabs> = (args) => <Tabs {...args} />
interface Props {
args: TabsProps
}
const items = [
{
title: 'First tab',
content: 'this is the content for the first tab'
},
{
title: 'Second tab',
content: 'this is the content for the second tab'
},
{
title: 'Third tab',
content: 'this is the content for the third tab'
}
]
export const Default = Template.bind({})
Default.args = {
items
}
export const WithRadio: Props = Template.bind({})
WithRadio.args = {
items,
showRadio: true
}
export const WithDefaultIndex: Props = Template.bind({})
WithDefaultIndex.args = {
items,
defaultIndex: 1
}
export const LotsOfTabs: Props = Template.bind({})
LotsOfTabs.args = {
items: items.flatMap((i) => [i, i, i])
}

View File

@ -0,0 +1,21 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { Default } from './index.stories'
describe('Tabs', () => {
test('should be able to change', async () => {
render(<Default {...Default.args} />)
fireEvent.click(screen.getByText('Second tab'))
const secondTab = await screen.findByText(/content for the second tab/i)
expect(secondTab).toBeInTheDocument()
})
test('should fire custom change handler', async () => {
const handler = jest.fn()
render(<Default {...Default.args} handleTabChange={handler} />)
fireEvent.click(screen.getByText('Second tab'))
expect(handler).toBeCalledTimes(1)
})
})

View File

@ -1,7 +1,7 @@
import React, { ReactElement, ReactNode } from 'react'
import React, { ReactElement, ReactNode, useState } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import InputElement from '@shared/FormInput/InputElement'
import styles from './Tabs.module.css'
import styles from './index.module.css'
import InputRadio from '@shared/FormInput/InputRadio'
export interface TabsItem {
title: string
@ -9,37 +9,36 @@ export interface TabsItem {
disabled?: boolean
}
export interface TabsProps {
items: TabsItem[]
className?: string
handleTabChange?: (tabName: string) => void
defaultIndex?: number
showRadio?: boolean
}
export default function Tabs({
items,
className,
handleTabChange,
defaultIndex,
showRadio
}: {
items: TabsItem[]
className?: string
handleTabChange?: (tabName: string) => void
defaultIndex?: number
showRadio?: boolean
}): ReactElement {
}: TabsProps): ReactElement {
return (
<ReactTabs
className={`${className && className}`}
defaultIndex={defaultIndex}
>
<ReactTabs className={`${className || ''}`} defaultIndex={defaultIndex}>
<TabList className={styles.tabList}>
{items.map((item, index) => (
<Tab
className={styles.tab}
key={item.title}
key={index}
onClick={handleTabChange ? () => handleTabChange(item.title) : null}
disabled={item.disabled}
>
{showRadio ? (
<InputElement
<InputRadio
name={item.title}
type="radio"
checked={defaultIndex === index}
checked={index === defaultIndex}
options={[item.title]}
readOnly
/>
@ -50,8 +49,8 @@ export default function Tabs({
))}
</TabList>
<div className={styles.tabContent}>
{items.map((item) => (
<TabPanel key={item.title}>{item.content}</TabPanel>
{items.map((item, index) => (
<TabPanel key={index}>{item.content}</TabPanel>
))}
</div>
</ReactTabs>

View File

@ -0,0 +1,40 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Tags, { TagsProps } from '@shared/atoms/Tags'
export default {
title: 'Component/@shared/atoms/Tags',
component: Tags
} as ComponentMeta<typeof Tags>
const Template: ComponentStory<typeof Tags> = (args) => <Tags {...args} />
interface Props {
args: TagsProps
}
export const Default: Props = Template.bind({})
Default.args = {
items: [' tag1 ', ' tag2 ', ' tag3 '],
className: 'custom-class'
}
export const MaxNumberOfTags: Props = Template.bind({})
MaxNumberOfTags.args = {
items: [' tag1 ', ' tag2 ', ' tag3 '],
max: 2
}
export const ShowMore: Props = Template.bind({})
ShowMore.args = {
items: [' tag1 ', ' tag2 ', ' tag3 '],
max: 2,
showMore: true
}
export const WithoutLinks: Props = Template.bind({})
WithoutLinks.args = {
items: [' tag1 ', ' tag2 ', ' tag3 '],
noLinks: true
}

View File

@ -1,8 +1,8 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import styles from './Tags.module.css'
import styles from './index.module.css'
declare type TagsProps = {
export interface TagsProps {
items: string[]
max?: number
showMore?: boolean

View File

@ -0,0 +1,37 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Time, { TimeProps } from '@shared/atoms/Time'
export default {
title: 'Component/@shared/atoms/Time',
component: Time
} as ComponentMeta<typeof Time>
const Template: ComponentStory<typeof Time> = (args) => <Time {...args} />
interface Props {
args: TimeProps
}
export const Default: Props = Template.bind({})
Default.args = {
date: '2022-05-02T11:50:28.000Z'
}
export const Relative: Props = Template.bind({})
Relative.args = {
date: '2022-05-02T11:50:28.000Z',
relative: true
}
export const IsUnix: Props = Template.bind({})
IsUnix.args = {
date: '1652448367',
isUnix: true
}
export const Undefined: Props = Template.bind({})
Undefined.args = {
date: null
}

View File

@ -1,19 +1,21 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { format, formatDistance } from 'date-fns'
export interface TimeProps {
date: string
relative?: boolean
isUnix?: boolean
displayFormat?: string
className?: string
}
export default function Time({
date,
relative,
isUnix,
displayFormat,
className
}: {
date: string
relative?: boolean
isUnix?: boolean
displayFormat?: string
className?: string
}): ReactElement {
}: TimeProps): ReactElement {
const [dateIso, setDateIso] = useState<string>()
const [dateNew, setDateNew] = useState<Date>()

View File

@ -3,7 +3,7 @@
}
.content {
composes: box from './Box.module.css';
composes: box from '../Box.module.css';
padding: calc(var(--spacer) / 4);
max-width: 25rem;
font-size: var(--font-size-small);

View File

@ -0,0 +1,51 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { TippyProps } from '@tippyjs/react'
import Tooltip from '@shared/atoms/Tooltip'
export default {
title: 'Component/@shared/atoms/Tooltip',
component: Tooltip
} as ComponentMeta<typeof Tooltip>
const Template: ComponentStory<typeof Tooltip> = (args) => <Tooltip {...args} />
interface Props {
args: TippyProps
}
export const Default: Props = Template.bind({})
Default.args = {
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.'
}
export const WithContentOpened: Props = Template.bind({})
WithContentOpened.args = {
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.',
showOnCreate: true
}
export const WithCustomTriggerElement: Props = Template.bind({})
WithCustomTriggerElement.args = {
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.',
children: <a>Tooltip trigger</a>
}
export const WithCustomTriggerEvent: Props = Template.bind({})
WithCustomTriggerEvent.args = {
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.',
children: <button>Click here</button>,
trigger: 'on click'
}
export const Disabled: Props = Template.bind({})
Disabled.args = {
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie.',
children: <a>Tooltip disabled</a>,
disabled: true
}

View File

@ -1,14 +1,8 @@
import React, { ReactElement, ReactNode } from 'react'
import classNames from 'classnames/bind'
import loadable from '@loadable/component'
import React, { ReactElement } from 'react'
import { useSpring, animated } from 'react-spring'
import styles from './Tooltip.module.css'
import stylesTooltip from './index.module.css'
import Info from '@images/info.svg'
import { Placement } from 'tippy.js'
const cx = classNames.bind(styles)
const Tippy = loadable(() => import('@tippyjs/react/headless'))
import Tippy, { TippyProps } from '@tippyjs/react/headless'
const animation = {
config: { tension: 400, friction: 20 },
@ -19,28 +13,15 @@ const animation = {
// Forward ref for Tippy.js
// eslint-disable-next-line
const DefaultTrigger = React.forwardRef((props, ref: any) => {
return <Info className={styles.icon} ref={ref} />
return <Info className={stylesTooltip.icon} ref={ref} />
})
export default function Tooltip({
content,
children,
trigger,
disabled,
className,
placement
}: {
content: ReactNode
children?: ReactNode
trigger?: string
disabled?: boolean
className?: string
placement?: Placement
}): ReactElement {
const [props, setSpring] = useSpring(() => animation.from)
export default function Tooltip(props: TippyProps): ReactElement {
const { content, children, trigger, disabled, className, placement } = props
const [styles, api] = useSpring(() => animation.from)
function onMount() {
setSpring({
api.start({
...animation.to,
onRest: (): void => null,
config: animation.config
@ -48,17 +29,14 @@ export default function Tooltip({
}
function onHide({ unmount }: { unmount: () => void }) {
setSpring({
api.start({
...animation.from,
onRest: unmount,
config: { ...animation.config, clamp: true }
})
}
const styleClasses = cx({
tooltip: true,
[className]: className
})
const styleClasses = `${stylesTooltip.tooltip} ${className || ''}`
return (
<Tippy
@ -68,23 +46,21 @@ export default function Tooltip({
trigger={trigger || 'mouseenter focus'}
disabled={disabled || null}
placement={placement || 'auto'}
render={(attrs: any) => (
<animated.div style={props}>
<div className={styles.content} {...attrs}>
render={(attrs) => (
<animated.div style={styles}>
<div className={stylesTooltip.content} {...attrs}>
{content}
<div className={styles.arrow} data-popper-arrow />
<div className={stylesTooltip.arrow} data-popper-arrow />
</div>
</animated.div>
)}
appendTo={
typeof document !== 'undefined' && document.querySelector('body')
}
animation
onMount={onMount}
onHide={onHide}
fallback={
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
}
// animation
{...props}
>
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
</Tippy>

View File

@ -1,5 +1,5 @@
.radioWrap {
composes: radioWrap from '@shared/FormInput/InputElement.module.css';
composes: radioWrap from '@shared/FormInput/InputRadio.module.css';
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 3);
border-bottom: 1px solid var(--border-color);
}
@ -9,14 +9,14 @@
}
.input {
composes: checkbox from '@shared/FormInput/InputElement.module.css';
composes: checkbox from '@shared/FormInput/InputRadio.module.css';
vertical-align: baseline;
margin-right: calc(var(--spacer) / 3);
margin-top: 0;
}
.radioLabel {
composes: radioLabel from '@shared/FormInput/InputElement.module.css';
composes: radioLabel from '@shared/FormInput/InputRadio.module.css';
font-weight: var(--font-weight-base);
display: flex;
align-items: center;

View File

@ -46,6 +46,7 @@ export default function Networks(): ReactElement {
trigger="click focus"
className={`${stylesIndex.preferences} ${styles.networks}`}
>
<>
<Network aria-label="Networks" className={stylesIndex.icon} />
<Caret aria-hidden="true" className={stylesIndex.caret} />
@ -54,6 +55,7 @@ export default function Networks(): ReactElement {
<span className={styles.chainsSelectedIndicator} key={chainId} />
))}
</div>
</>
</Tooltip>
)
}

View File

@ -28,8 +28,10 @@ export default function UserPreferences(): ReactElement {
trigger="click focus"
className={styles.preferences}
>
<>
<Cog aria-label="Preferences" className={styles.icon} />
<Caret aria-hidden="true" className={styles.caret} />
</>
</Tooltip>
)
}

View File

@ -27,7 +27,7 @@ const columns = [
selector: function getAssetRow(row: AssetExtended) {
return (
<Tooltip content={row.datatokens[0].name}>
{row.datatokens[0].symbol}
<>{row.datatokens[0].symbol}</>
</Tooltip>
)
},
@ -96,7 +96,11 @@ export default function Bookmarks(): ReactElement {
columns={columns}
data={pinned}
isLoading={isLoading}
emptyMessage="Your bookmarks will appear here."
emptyMessage={
chainIds.length === 0
? 'No network selected'
: 'Your bookmarks will appear here.'
}
noTableHead
/>
)

View File

@ -123,6 +123,7 @@ export default function ComputeJobs({
isLoading={isLoading}
defaultSortField="row.dateCreated"
defaultSortAsc={false}
emptyMessage={chainIds.length === 0 ? 'No network selected' : null}
/>
</>
) : (

View File

@ -4,7 +4,7 @@ import Time from '@shared/atoms/Time'
import AssetTitle from '@shared/AssetList/AssetListTitle'
import NetworkName from '@shared/NetworkName'
import { useProfile } from '@context/Profile'
import { useUserPreferences } from '@context/UserPreferences'
const columns = [
{
name: 'Data Set',
@ -38,6 +38,7 @@ export default function ComputeDownloads({
accountId: string
}): ReactElement {
const { downloads, isDownloadsLoading } = useProfile()
const { chainIds } = useUserPreferences()
return accountId ? (
<Table
@ -45,6 +46,7 @@ export default function ComputeDownloads({
data={downloads}
paginationPerPage={10}
isLoading={isDownloadsLoading}
emptyMessage={chainIds.length === 0 ? 'No network selected' : null}
/>
) : (
<div>Please connect your Web3 wallet.</div>

View File

@ -109,6 +109,7 @@ export default function PoolShares({
isLoading={loading}
sortField="userLiquidity"
sortAsc={false}
emptyMessage={chainIds.length === 0 ? 'No network selected' : null}
/>
) : (
<div>Please connect your Web3 wallet.</div>