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';