diff --git a/src/lib/components/base-table/base-table.stories.tsx b/src/lib/components/base-table/base-table.stories.tsx new file mode 100644 index 00000000..f66a556f --- /dev/null +++ b/src/lib/components/base-table/base-table.stories.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { BaseTable, BaseTableProps } from './base-table'; +import { Meta, StoryFn } from '@storybook/react'; +import TableDataHeader from '../table-data-header/table-data-header'; +import TableRow from '../table-row/table-row'; +import TableData from '../table-data/table-data'; +import BodyText from '../body-text/body-text'; +import { PrecisionCase } from '../../utils/currency'; +import Cspr from '../cspr/cspr'; +import PageTile from '../page-tile/page-tile'; + +const mockedData = [ + { rank: 1, motes: '50000000000000', owner: 'konrad.cspr' }, + { rank: 2, motes: '482900000000000', owner: 'victoria.cspr' }, + { rank: 3, motes: '1000000', owner: 'ab.cspr' }, +]; + +export default { + component: BaseTable, + title: 'Components/Table/Base Table', + args: { + renderDataHeaders: () => ( + + Rank + Balance{' '} + Owner + + ), + renderData: () => ( + <> + {mockedData.map((data) => ( + + + {data.rank} + + + + + + + + {data.owner} + + + ))} + + ), + }, +} as Meta; + +const Template: StoryFn = (args: BaseTableProps) => { + return ( + + + + ); +}; + +export const Primary = Template.bind({}); diff --git a/src/lib/components/base-table/base-table.tsx b/src/lib/components/base-table/base-table.tsx new file mode 100644 index 00000000..df322d2e --- /dev/null +++ b/src/lib/components/base-table/base-table.tsx @@ -0,0 +1,70 @@ +import React, { ReactNode } from 'react'; +import styled from 'styled-components'; +import TableHead from '../table-head/table-head'; +import TableBody from '../table-body/table-body'; +import BodyText from '../body-text/body-text'; + +export interface BaseTableProps { + renderHeader?: () => ReactNode; + renderDataHeaders?: () => ReactNode; + renderData?: () => ReactNode; + renderFooter?: () => ReactNode; + noData?: boolean; + noDataMessage?: string; + paddingBottom?: number; +} + +export const TableContainer = styled.div<{ paddingBottom?: number }>( + ({ theme, paddingBottom }) => ({ + overflowX: 'auto', + ...(paddingBottom && { paddingBottom }), + }), +); + +const StyledTable = styled.table(({ theme }) => ({ + width: '100%', + position: 'relative', + borderCollapse: 'collapse', +})); + +const NoDataContainer = styled.div(({ theme }) => ({ + position: 'absolute', + top: 0, + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +export function BaseTable(props: BaseTableProps) { + const { + renderHeader, + renderDataHeaders, + renderData, + renderFooter, + noData, + noDataMessage, + paddingBottom, + } = props; + + return ( + <> + {renderHeader && renderHeader()} + + + {renderDataHeaders && {renderDataHeaders()}} + {renderData && {renderData()}} + + + {renderFooter && renderFooter()} + {noDataMessage && noData && ( + + {noDataMessage} + + )} + + ); +} + +export default BaseTable; diff --git a/src/lib/components/pagination/pagination-button.tsx b/src/lib/components/pagination/pagination-button.tsx new file mode 100644 index 00000000..beebbb41 --- /dev/null +++ b/src/lib/components/pagination/pagination-button.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import styled from 'styled-components'; +import Button, { ButtonProps } from '../button/button'; +import CaptionText from '../caption-text/caption-text'; + +interface PaginationButtonProps extends ButtonProps { + children?: React.ReactNode; +} + +const StyledButton = styled(Button)(({ theme }) => + theme.withMedia({ + width: 'auto', + fontWeight: theme.typography.fontWeight.medium, + minHeight: 24, + padding: ['2px 10px'], + }), +); + +const StyledArrowsButton = styled(StyledButton)(({ theme }) => + theme.withMedia({ + padding: ['2px 4px'], + }), +); + +export const PaginationButton = ({ + children, + ...restProps +}: PaginationButtonProps) => { + return ( + + {children} + + ); +}; + +export const PaginationArrowButton = ({ + children, + ...restProps +}: PaginationButtonProps) => { + return ( + + {children} + + ); +}; diff --git a/src/lib/components/pagination/pagination-container.tsx b/src/lib/components/pagination/pagination-container.tsx new file mode 100644 index 00000000..6d619bd2 --- /dev/null +++ b/src/lib/components/pagination/pagination-container.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const PaginationContainer = styled.div(({ theme }) => + theme.withMedia({ + border: 'none', + cursor: 'pointer', + color: theme.styleguideColors.contentRed, + background: theme.styleguideColors.fillSecondary, + borderRadius: theme.borderRadius.base, + fontWeight: theme.typography.fontWeight.medium, + minHeight: 24, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: ['2px 8px'], + ':hover': { + background: theme.styleguideColors.fillSecondaryRedHover, + color: theme.styleguideColors.fillPrimaryRedHover, + }, + ':active': { + background: theme.styleguideColors.fillSecondaryRedClick, + color: theme.styleguideColors.fillPrimaryRedClick, + }, + }), +); diff --git a/src/lib/components/pagination/pagination-dropdown.tsx b/src/lib/components/pagination/pagination-dropdown.tsx new file mode 100644 index 00000000..ace4d5f1 --- /dev/null +++ b/src/lib/components/pagination/pagination-dropdown.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { PaginationContainer } from './pagination-container'; +import { useClickAway } from '../../hooks/use-click-away'; +import CaptionText from '../caption-text/caption-text'; +import SvgIcon from '../svg-icon/svg-icon'; +import ArrowDownIcon from '../../assets/icons/ic-arrow-down.svg'; + +export const PaginationDropdownContainer = styled.div(({ theme }) => + theme.withMedia({ + position: 'relative', + minWidth: [58], + }), +); + +export const PaginationDropdownMenu = styled.ul(({ theme }) => + theme.withMedia({ + width: '100%', + position: 'absolute', + display: 'block', + background: theme.styleguideColors.fillSecondary, + boxShadow: theme.boxShadow.block, + padding: 0, + margin: '4px 0', + borderRadius: theme.borderRadius.base, + zIndex: theme.zIndex.dropdown, + '& > div': { + borderRadius: 0, + }, + '& > :first-child': { + borderRadius: theme.borderRadius.base, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + }, + '& > :last-child': { + borderRadius: theme.borderRadius.base, + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + }, + }), +); + +const PaginationDropdownMenuItem = styled.li(({ theme }) => ({ + alignItems: 'center', + display: 'flex', + position: 'relative', + '& > input': { + display: 'none', + }, +})); + +interface PaginationDropdownProps { + value: number; + items: number[]; + onChange: (perPage: number) => void; +} + +export const PaginationDropdown = ({ + value, + items, + onChange, +}: PaginationDropdownProps) => { + const [opened, setOpened] = useState(false); + + const { ref } = useClickAway({ + callback: () => { + setOpened(false); + }, + }); + + return ( + { + setOpened(!opened); + }} + > + + {value} + + + {opened && ( + + {items.map((item) => ( + { + onChange(item); + }} + > + + + {item} + + + ))} + + )} + + ); +}; diff --git a/src/lib/components/pagination/pagination-info-text.tsx b/src/lib/components/pagination/pagination-info-text.tsx new file mode 100644 index 00000000..dc983690 --- /dev/null +++ b/src/lib/components/pagination/pagination-info-text.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styled from 'styled-components'; +import CaptionText from '../caption-text/caption-text'; +import FlexBox from '../flex-box/flex-box'; + +interface PaginationInfoTextProps { + children?: React.ReactNode; +} + +export const StyledContainer = styled(FlexBox)(({ theme }) => + theme.withMedia({ + textAlign: 'center', + borderRadius: theme.borderRadius.base, + backgroundColor: theme.styleguideColors.fillSecondary, + color: theme.styleguideColors.contentPrimary, + height: 20, + padding: ['4px 8px', '4px 16px'], + width: '100%', + }), +); + +export const PaginationInfoText = ({ + children, + ...props +}: PaginationInfoTextProps) => { + return ( + + + {children} + + + ); +}; diff --git a/src/lib/components/pagination/pagination-input.tsx b/src/lib/components/pagination/pagination-input.tsx new file mode 100644 index 00000000..6ecefef1 --- /dev/null +++ b/src/lib/components/pagination/pagination-input.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { PaginationInfoText } from './pagination-info-text'; +import { ButtonProps } from '../button/button'; +import { InputProps } from '../input/input'; +import { useClickAway } from '../../hooks/use-click-away'; +import { formatNumber } from '../../utils/formatters'; + +interface PaginationInputProps extends ButtonProps { + currentPage: number; + pageCount: number; + onChange: (page) => void; +} + +export const PaginationInputContainer = styled.div(({ theme }) => + theme.withMedia({ + width: ['unset', '160px', '160px'], + '*': { + boxSizing: 'border-box', + }, + }), +); + +const StyledInput = styled('input')(({ theme }) => ({ + color: 'inherit', + background: 'inherit', + fontFamily: 'inherit', + fontSize: 'inherit', + border: 'none', + width: '100%', + padding: 0, + textAlign: 'center', + caretColor: theme.styleguideColors.contentRed, + '&[type=number]': { + '&::-webkit-inner-spin-button, &::-webkit-outer-spin-button': { + margin: 0, + '-webkit-appearance': 'none', + 'pointer-events': 'none', + }, + }, + outline: 'none', +})); + +const InputInfoText = styled(PaginationInfoText)(({ theme }) => ({ + width: '100%', + height: 24, + cursor: 'pointer', + ':hover': { + background: theme.styleguideColors.fillSecondaryRedHover, + color: theme.styleguideColors.contentRed, + }, +})); + +export const PaginationInput = ({ + currentPage, + pageCount, + onChange, +}: PaginationInputProps) => { + const [isHovered, setHover] = useState(false); + const [showInput, setShowInput] = useState(false); + const [page, setPage] = useState(undefined); + + const convertDecimalToThousand = (value) => { + return Number(value?.replace(/[,.]/g, '')) || 0; + }; + + const resetInputValue = () => { + setShowInput(false); + setPage(undefined); + }; + + const { ref } = useClickAway({ + callback: () => { + resetInputValue(); + }, + }); + + const handleMouseOver = () => { + setHover(true); + }; + + const handleMouseOut = () => { + setHover(false); + }; + + const handleClick = () => { + setShowInput(true); + }; + + const handleSubmit = (e) => { + if (e.keyCode === 13) { + const pageNumber = convertDecimalToThousand(page); + + onChange(pageNumber > pageCount ? pageCount : pageNumber); + resetInputValue(); + } + }; + + return ( + + + {showInput ? ( + setPage(e.target.value)} + onKeyDown={handleSubmit} + /> + ) : isHovered && !showInput ? ( + <>Enter page + ) : ( + <> + Page {formatNumber(currentPage)} of {formatNumber(pageCount)} + + )} + + + ); +}; diff --git a/src/lib/components/pagination/pagination.stories.tsx b/src/lib/components/pagination/pagination.stories.tsx new file mode 100644 index 00000000..1ec5c39d --- /dev/null +++ b/src/lib/components/pagination/pagination.stories.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Meta, StoryFn } from '@storybook/react'; +import { Pagination } from './pagination'; +import PageTile from "../page-tile/page-tile"; + +export default { + component: Pagination, + title: 'Components/Table/Pagination', + args: { + itemCount: 50, + pageCount: 5, + }, +} as Meta; + +const Template: StoryFn = (args) => { + const [pagination, setPagination] = useState({ + currentPage: 1, + pageSize: 10, + }); + + const handleCurrentPage = (page: number) => { + setPagination((prev) => ({ + ...prev, + currentPage: page, + })); + } + const handlePerPage = (pageSize: number) => { + setPagination((prev) => ({ + ...prev, + pageSize: pageSize, + })); + } + return ( + + + + ); +} + +export const Primary = Template.bind({}); diff --git a/src/lib/components/pagination/pagination.tsx b/src/lib/components/pagination/pagination.tsx new file mode 100644 index 00000000..7cb201dc --- /dev/null +++ b/src/lib/components/pagination/pagination.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { PaginationArrowButton, PaginationButton } from './pagination-button'; +import { PaginationInput } from './pagination-input'; +import { RowsPerPage } from './rows-per-page'; +import FlexRow from '../flex-row/flex-row'; +import SvgIcon from '../svg-icon/svg-icon'; +import FlexColumn from '../flex-column/flex-column'; +import { formatNumber } from '../../utils/formatters'; +import BodyText from '../body-text/body-text'; +import { useMatchMedia } from '../../utils/match-media'; +import ArrowRightIcon from '../../assets/icons/ic-arrow-right.svg'; + +const ROWS_PER_PAGE = [5, 10, 25, 50]; + +export const Container = styled(FlexRow)(({ theme }) => + theme.withMedia({ + height: [80, 48], + flexDirection: ['column', 'row', 'row'], + justifyContent: 'space-between', + padding: ['12px 10px', '12px 20px'], + margin: ['0 0 10px 0', '0'], + }), +); + +const MirroredSvgIcon = styled(SvgIcon)(({ theme }) => ({ + transform: 'rotate(180deg)', +})); + +export interface PaginationProps { + perPage: number; + itemCount?: number; + pageCount?: number; + currentPage?: number; + setPerPage?: (limit: number) => void; + setCurrentPage?: (page: number) => void; + hideRowsPerPage?: boolean; + totalRowsLabel?: string; +} + +export const Pagination: React.FC = ({ + perPage = 10, + itemCount= 0, + pageCount = 1, + currentPage = 1, + setCurrentPage = () => {}, + setPerPage = () => {}, + hideRowsPerPage = false, + totalRowsLabel = 'total row', +}) => { + const isFirstEnabled = currentPage > 1; + const isPrevEnabled = currentPage > 1; + const isLastEnabled = currentPage < pageCount; + const noData = pageCount < 1; + + const prevPageHandler = () => setCurrentPage(currentPage - 1); + + const nextPageHandler = () => setCurrentPage(currentPage + 1); + + const firstPageHandler = () => setCurrentPage(1); + + const lastPageHandler = () => setCurrentPage(pageCount); + + const handlePaginationChange = (page: number) => + setCurrentPage(page); + + const onMobile = ( + + + + First + + + + + + + + + + Last + + + {!hideRowsPerPage && ( + + )} + + ); + + const onAbove = ( + + {!hideRowsPerPage && ( + + )} + + + First + + + + + + + + + + Last + + + + ); + + const responsivePagination = useMatchMedia( + [onMobile, onAbove], + [perPage, currentPage, pageCount], + ); + + if (noData) { + return ( + +   + + ); + } + return ( + + + + {formatNumber(itemCount)} {totalRowsLabel} + {itemCount > 1 && 's'} + + + {responsivePagination} + + ); +}; + +export default Pagination; diff --git a/src/lib/components/pagination/rows-per-page.tsx b/src/lib/components/pagination/rows-per-page.tsx new file mode 100644 index 00000000..a4710e85 --- /dev/null +++ b/src/lib/components/pagination/rows-per-page.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { PaginationInfoText } from './pagination-info-text'; +import { PaginationDropdown } from './pagination-dropdown'; +import FlexRow from '../flex-row/flex-row'; + +export const RowsPerPage = ({ + value, + items, + onChange, +}: { + value: number; + items: number[]; + onChange: (perPage) => void; +}) => { + return ( + + Show rows + + + ); +}; diff --git a/src/lib/components/table-data-header/table-data-header.stories.tsx b/src/lib/components/table-data-header/table-data-header.stories.tsx new file mode 100644 index 00000000..7667fcaa --- /dev/null +++ b/src/lib/components/table-data-header/table-data-header.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Meta, StoryFn } from '@storybook/react'; +import TableDataHeader, { + TableDataHeaderProps, +} from './table-data-header'; + +export default { + component: TableDataHeader, + title: 'Components/Table/Table Data Header', +} as Meta; + +const Template: StoryFn = ( + args: TableDataHeaderProps, +) => { + return Name; +}; + +export const Primary = Template.bind({}); diff --git a/src/lib/components/table-data-header/table-data-header.tsx b/src/lib/components/table-data-header/table-data-header.tsx new file mode 100644 index 00000000..7e626e37 --- /dev/null +++ b/src/lib/components/table-data-header/table-data-header.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { BaseProps } from '../../types'; +import BodyText from '../body-text/body-text'; +import Tooltip from '../tooltip/tooltip'; +import FlexRow from '../flex-row/flex-row'; + +export interface TableDataHeaderProps extends BaseProps { + align?: 'left' | 'right' | 'center'; + fitContent?: boolean; + tooltipText?: JSX.Element | string | undefined; + icon?: JSX.Element | undefined; + fixedWidthRem?: number; +} + +const StyledTableDataHeader = styled.th( + ({ theme, align = 'left', fitContent, fixedWidthRem }) => ({ + textAlign: align, + height: 20, + padding: 8, + ':first-of-type': { + paddingLeft: 20, + }, + ':last-of-type': { + paddingRight: 20, + }, + ...(fitContent && { + width: '1%', + }), + ...(fixedWidthRem && { + width: `${fixedWidthRem}rem`, + }), + textTransform: 'capitalize', + }), +); + +const StyledHeaderGroup = styled.div(({ theme }) => ({ + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', +})); + +export function TableDataHeader({ + children, + tooltipText, + icon, + ...restProps +}: TableDataHeaderProps) { + return ( + + + + + + {children} + + {icon} + + + + + ); +} + +export default TableDataHeader; diff --git a/src/lib/components/table/table-error.tsx b/src/lib/components/table/table-error.tsx new file mode 100644 index 00000000..2cb7f82f --- /dev/null +++ b/src/lib/components/table/table-error.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import styled from 'styled-components'; +import BodyText from '../body-text/body-text'; +import SvgIcon from '../svg-icon/svg-icon'; + +const FailedToFetchWrapper = styled('div')(({ theme }) => ({ + height: 400, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', +})); + +const StyledBodyText = styled(BodyText)(({ theme }) => ({ + marginTop: 8, + color: theme.styleguideColors.contentSecondary, +})); + +const StyledSvgIcon = styled(SvgIcon)(({ theme }) => ({ + path: { + fill: theme.styleguideColors.fillPrimaryRed, + }, + marginBottom: 20, +})); + +export const FailedToFetch = () => ( + + + + Failed to load data + + + Please try again later + + +); + +export interface TableErrorProps { + columnsLength?: number; +} + +export const TableError = ({ columnsLength }: TableErrorProps) => { + return ( + + + + + + ); +}; diff --git a/src/lib/components/table/table-loader.tsx b/src/lib/components/table/table-loader.tsx new file mode 100644 index 00000000..62174c85 --- /dev/null +++ b/src/lib/components/table/table-loader.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import styled from 'styled-components'; +import Skeleton from 'react-loading-skeleton'; +import { matchSize } from '../../utils/match-size'; +import { TableRowType, TableRow } from '../table-row/table-row'; +import TableData from '../table-data/table-data'; + +type Props = { + columnsLength: number; + rowsLength?: number; + tableRowType?: TableRowType; +}; + +type TableLoaderRowProps = { + type: TableRowType; +}; + +const TableLoaderRow = styled(TableRow)( + ({ theme, type }) => ({ + height: matchSize( + { + [TableRowType.TextWithAvatar]: '56px', + [TableRowType.TextWithIcon]: '52px', + [TableRowType.TextSingleLine]: '48px', + }, + type, + ), + }), +); + +export function TableLoader({ + columnsLength, + rowsLength = 10, + tableRowType = TableRowType.TextSingleLine, +}: Props) { + const tableData = Array(rowsLength).fill(undefined); + const columnsRow = Array(columnsLength).fill(null); + + return ( + <> + {tableData.map((item, index) => ( + + {columnsRow.map((item2, index2) => ( + + + + ))} + + ))} + + ); +} diff --git a/src/lib/components/table/table.stories.tsx b/src/lib/components/table/table.stories.tsx new file mode 100644 index 00000000..0ff6a942 --- /dev/null +++ b/src/lib/components/table/table.stories.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Table, TableProps } from './table'; +import { Meta, StoryFn } from '@storybook/react'; +import HeaderText from '../header-text/header-text'; +import TableDataHeader from '../table-data-header/table-data-header'; +import TableRow from '../table-row/table-row'; +import TableData from '../table-data/table-data'; +import BodyText from '../body-text/body-text'; +import { PrecisionCase } from '../../utils/currency'; +import Cspr from '../cspr/cspr'; +import PageTile from '../page-tile/page-tile'; +import FlexRow from "../flex-row/flex-row"; + +export default { + component: Table, + title: 'Components/Table/Table with pagination', + args: { + renderHeader: () => ( + + Table with pagination + + ), + renderDataHeaders: () => ( + + Rank + Balance{' '} + Owner + + ), + renderPaginatedData: (data) => ( + <> + {data.map((data) => ( + + + {data.rank} + + + + + + + + {data.owner} + + + ))} + + ), + }, +} as Meta; + +const getRandomBalance = () => { + const min = 100000000000; + const max = 10000000000000; + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +const MOCKED_OWNERS = [ + "Alice","Bob","Charlie","Diana","Ethan","Fiona","Gabe","Hana","Ivan","Jules", + "Kira","Liam","Mona","Noah","Olga","Pavel","Quinn","Ravi","Sara","Tara", + "Uma","Vik","Walt","Xena","Yara","Zane" +]; + +const MOCK_DATA = Array.from({ length: 87 }, (_, i) => { + const rank = i + 1; + const owner = MOCKED_OWNERS[i % MOCKED_OWNERS.length]; + const csprName = `${owner.toLowerCase()}.cspr` + const motes = getRandomBalance(); + return { rank, owner: csprName, motes }; +}); + +const emulateGetTableDataRequest = (url: string) => { + const u = new URL(url, "https://example.local"); + const page = Math.max(parseInt(u.searchParams.get("page") || "1", 10), 1); + const pageSize = Math.max(parseInt(u.searchParams.get("page_size") || "10", 10), 1); + + const items_count = MOCK_DATA.length; + const page_count = Math.max(1, Math.ceil(items_count / pageSize)); + + const start = (page - 1) * pageSize; + const end = start + pageSize; + const data = MOCK_DATA.slice(start, end); + + return new Promise((resolve) => { + setTimeout(() => { + resolve({ data, page_count, items_count }); + }, 400); + }); +} + +const Template: StoryFn = (args: TableProps) => { + const [data, setData] = useState(null) + const [pagination, setPagination] = useState({ + currentPage: 1, + pageSize: 10, + }); + + const requestPath = useMemo( + () => `/api/table?page=${pagination.currentPage}&page_size=${pagination.pageSize}`, + [pagination.currentPage, pagination.pageSize] + ); + + useEffect(() => { + (async () => { + const res = await emulateGetTableDataRequest(requestPath); + setData(res); + })(); + }, [requestPath]); + + const handleCurrentPage = (page: number) => { + setPagination((prev) => ({ + ...prev, + currentPage: page, + })); + } + const handlePerPage = (pageSize: number) => { + setPagination((prev) => ({ + ...prev, + pageSize: pageSize, + })); + } + return ( + + + Emulated api request: {requestPath} + + args.renderPaginatedData(data?.data)} + itemCount={data?.items_count} + pageCount={data?.page_count} + currentPage={pagination.currentPage} + perPage={pagination.pageSize} + setCurrentPage={handleCurrentPage} + setPerPage={handlePerPage} + /> + + ); +}; + +export const Primary = Template.bind({}); diff --git a/src/lib/components/table/table.tsx b/src/lib/components/table/table.tsx new file mode 100644 index 00000000..2d6382fa --- /dev/null +++ b/src/lib/components/table/table.tsx @@ -0,0 +1,104 @@ +import React, { Children } from 'react'; + +import { BaseTable } from '../base-table/base-table'; +import { Pagination } from '../pagination/pagination'; +import { TableRowType } from '../table-row/table-row'; +import { TableLoader } from './table-loader'; +import { TableError } from './table-error'; + +export enum OrderDirection { + ASC = 'ASC', + DESC = 'DESC', +} + +export interface ErrorResult { + code: string; + message: string; + description?: string | React.ReactElement; +} + +export interface SortingProps { + orderBy?: string | undefined; + orderDirection?: OrderDirection; + setOrder?: (orderBy: string | undefined, direction: OrderDirection) => void; + reverseSortingDirection?: boolean; +} + +export type RenderProps = { sortingProps: SortingProps }; + +export type TableProps = { + data: null | Entity[]; + loading?: boolean; + error?: ErrorResult | null; + renderDataHeaders: (renderProps: RenderProps) => React.ReactElement; + renderPaginatedData: ( + paginatedData: Entity[], + renderProps?: RenderProps, + ) => React.ReactElement | React.ReactElement[]; + tableRowType?: TableRowType; + pageCount: number; + currentPage: number; + pageSize?: number; + itemCount?: number; + setPerPage?: (limit: number) => void; + setCurrentPage?: (page: number) => void; + hideRowsPerPage?: boolean; + totalRowsLabel?: string; +}; + +export const Table = ({ + data, + loading, + error, + renderDataHeaders, + renderPaginatedData, + tableRowType = TableRowType.TextWithAvatar, + ...props +}: TableProps) => { + const renderPaginationRow = () => + !error && ( + + ); + + return ( + renderPaginationRow()} + renderDataHeaders={() => + renderDataHeaders({ + sortingProps: null!, + }) + } + renderData={() => + (data == null && !error) || loading ? ( + + ) : error ? ( + + ) : data ? ( + renderPaginatedData(data, { + sortingProps: {}!, + }) + ) : ( + <> + ) + } + renderFooter={() => renderPaginationRow()} + {...props} + /> + ); +}; + +export default Table; diff --git a/src/lib/index.ts b/src/lib/index.ts index 18155aa9..64eaf6c1 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -34,6 +34,11 @@ export * from './components/table-body/table-body'; export * from './components/table-data/table-data'; export * from './components/table-head/table-head'; export * from './components/table-row/table-row'; +export * from './components/base-table/base-table'; +export * from './components/table-data-header/table-data-header'; +export * from './components/table/table'; +export * from './components/table/table-error'; +export * from './components/table/table-loader'; export * from './components/textarea/textarea'; export * from './components/truncate-box/truncate-box'; export * from './components/text/text';