Skip to content

Commit

Permalink
Added table component and some glob constants
Browse files Browse the repository at this point in the history
  • Loading branch information
belousovjr committed Nov 14, 2023
1 parent c606cfd commit 16f8078
Show file tree
Hide file tree
Showing 13 changed files with 487 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/ResetCSS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ResetCSS = createGlobalStyle`
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) => theme.colors.lightTextGray};
Expand Down
7 changes: 4 additions & 3 deletions src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Box, Grid } from "../Box";
import { useEffect, useMemo, useRef, useState } from "react";
import { useOnClickOutside } from "../../hooks";
import { ArrowDownIcon } from "../Svg";
import { DropdownWrapper, DropdownActivatorWrapper, DropdownItemWrapper } from './styles';
import { IDropdownProps } from './types';
import { DropdownWrapper, DropdownActivatorWrapper, DropdownItemWrapper } from "./styles";
import { IDropdownProps } from "./types";
import { Z_INDEX } from "../../constants";

export default function Dropdown({ items, value, onChange }: IDropdownProps) {
const activeItem = useMemo(() => items.find((v) => v.value === value), [items, value]);
Expand All @@ -27,7 +28,7 @@ export default function Dropdown({ items, value, onChange }: IDropdownProps) {
const height = useMemo(() => (opened ? maxHeight : minHeight), [opened, maxHeight]);

return (
<Box height={`${minHeight}px`} overflow={"visible"} position={"relative"} zIndex={100}>
<Box height={`${minHeight}px`} overflow={"visible"} position={"relative"} zIndex={Z_INDEX.DROPDOWN}>
<DropdownWrapper
ref={listRef}
maxHeight={height}
Expand Down
14 changes: 14 additions & 0 deletions src/components/Svg/Icons/SortIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import Svg from "../Svg";
import { SvgProps } from "../types";

const Icon: React.FC<React.PropsWithChildren<SvgProps>> = (props) => (
<Svg viewBox="0 0 12 12" {...props} fill="none">
<path d="M5.25 9L3.75 10.5L2.25 9" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.75 3L8.25 1.5L9.75 3" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.25 10.5V1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.75 1.5V10.5" strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);

export default Icon;
1 change: 1 addition & 0 deletions src/components/Svg/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as ArrowLeftIcon } from "./Icons/ArrowLeftIcon";
export { default as QuestionIcon } from "./Icons/QuestionIcon";
export { default as InfoIcon } from "./Icons/InfoIcon";
export { default as ArrowDownIcon } from "./Icons/ArrowDownIcon";
export { default as SortIcon } from "./Icons/SortIcon";
133 changes: 133 additions & 0 deletions src/components/Table/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import TableComponent from "./index";
import React, { useState } from "react";
import { Box, Flex, Grid } from "../Box";
import Button from "../Button";
import Text from "../Text";
import Dropdown from "../Dropdown";
import Checkbox from "../Checkbox";
import Tooltip from "../Tooltip";

export default {
title: "Components/Table",
component: TableComponent,
argTypes: {},
};

export const Table = () => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(3);
const [withMinHeight, setWithMinHeight] = useState(true);
const [showCustomHeader, setShowCustomHeader] = useState(true);
const [loading, setLoading] = useState(false);
const [empty, setEmpty] = useState(false);

const items = [
{ id: 0, price: 12, name: "H Name", someField: "Some Field Value", sortField: "123" },
{ id: 1, price: 54, name: "D Name", someField: "Some Field Value", sortField: "Some String" },
{ id: 2, price: 1, name: "I Name", someField: "Some Field Value", sortField: "Some String" },
{
id: 3,
price: 31,
name: "G Name",
someField: "Some Field Value",
sortField: "123",
},
{ id: 4, price: 82, name: "J Name", someField: "Some Field Value", sortField: "Some String" },
{ id: 5, price: 51, name: "F Name", someField: "Some Field Value", sortField: "123" },
{ id: 6, price: 66, name: "C Name", someField: "Some Field Value", sortField: "Some String" },
{ id: 7, price: 89, name: "A Name", someField: "Some Field Value", sortField: "Some String" },
{ id: 8, price: 5, name: "E Name", someField: "Some Field Value", sortField: "123" },
{ id: 9, price: 57, name: "B Name", someField: "Some Field Value", sortField: "Some String" },
];

type ItemType = (typeof items)[number];

return (
<Box>
<Grid gridTemplateColumns={"repeat(2, auto)"} gridGap={"16px"} alignItems="center" mb={"24px"}>
<Flex alignItems="center">
<Text mr={"8px"}>Per page:</Text>
<Dropdown
value={perPage}
onChange={setPerPage}
items={[
{ value: 3, title: "3 Items" },
{ value: 5, title: "5 Items" },
{ value: 10, title: "10 Items" },
]}
/>
</Flex>
<label style={{ cursor: "pointer", display: "flex" }}>
<Text mr={"8px"}>Min height (347px)</Text>
<Checkbox value={withMinHeight} onChange={setWithMinHeight} />
</label>
<label style={{ cursor: "pointer", display: "flex" }}>
<Text mr={"8px"}>Custom header</Text>
<Checkbox value={showCustomHeader} onChange={setShowCustomHeader} />
</label>
<label style={{ cursor: "pointer", display: "flex" }}>
<Text mr={"8px"}>Loading</Text>
<Checkbox value={loading} onChange={setLoading} />
</label>
<label style={{ cursor: "pointer", display: "flex" }}>
<Tooltip content={"Empty List card can be customized"}>
<Text mr={"8px"}>Empty list</Text>
</Tooltip>
<Checkbox value={empty} onChange={setEmpty} />
</label>
</Grid>
<TableComponent<ItemType>
headers={[
{ key: "name", title: "Name", sortable: true },
{
key: "sortField",
title: "Special Field",
tooltip: "Values are sorted based on convertibility to number with custom sorting method",
sortFunc: (a, b, reverseOrder) =>
(Number(isNaN(Number(a.sortField))) - Number(isNaN(Number(b.sortField)))) * (reverseOrder ? -1 : 1),
},
{
key: "someField",
title: "Some Field",
},

{
key: "price",
title: "Price $",
width: "100px",
sortable: true,
tooltip: "Custom width === 100px",
renderFunc: ({ price }) => `$${price}`,
},
{
width: "77px",
renderFunc: (item) => (
<Button
onClick={() => {
console.log("Click table item", item);
}}
scale={"small"}
variant={"outlined"}
>
Click
</Button>
),
}, //actions
]}
items={!empty && !loading ? items : []}
page={page}
perPage={perPage}
changePage={setPage}
header={
showCustomHeader ? (
<Text pb={"24px"} variant="h5">
Custom header
</Text>
) : undefined
}
minHeight={withMinHeight ? "347px" : undefined}
loading={loading}
/>
</Box>
);
};
164 changes: 164 additions & 0 deletions src/components/Table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {
TableContent,
TableContentWrap,
TableHeader,
TableHeaderTitleBtn,
TableItem,
TablePagBtn,
TablePagWrap,
TableWrap,
} from "./styles";
import { ITableProps, SortData } from "./types";
import Text from "../Text";
import { ArrowLeftIcon, QuestionIcon } from "../Svg";
import Tooltip from "../Tooltip";
import { Box, Flex } from "../Box";
import React, { useEffect, useMemo, useState } from "react";
import { useTheme } from "styled-components";
import Svg from "../Svg/Svg";
import { defaultSortCallback } from "./utils";
import { LoadingSpinner } from "../Loaders";

function SortBtn({ data, active }: { data: SortData<any> | undefined; active: boolean }) {
const theme = useTheme();
const directColor = active && data && !data.reverseOrder ? theme.colors.primaryDefault : theme.colors.darkGray;
const reverseColor = active && data?.reverseOrder ? theme.colors.primaryDefault : theme.colors.darkGray;
return (
<Svg viewBox="0 0 12 12" size={"12px"} mr={"4px"} fill="none">
<path d="M5.25 9L3.75 10.5L2.25 9" stroke={directColor} strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.75 3L8.25 1.5L9.75 3" stroke={reverseColor} strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.25 10.5V1.5" stroke={reverseColor} strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.75 1.5V10.5" stroke={directColor} strokeLinecap="round" strokeLinejoin="round" />
</Svg>
);
}

export default function Table<T = any>({
headers,
items,
page,
perPage = 10,
minHeight,
header,
changePage = (page: number) => {},
loading,
emptyCard,
}: ITableProps<T>) {
const maxPage = useMemo(() => Math.ceil(items.length / perPage), [items, perPage]);

const theme = useTheme();

const [sortData, setSortData] = useState<SortData<T>>();

const sortedItems = useMemo(() => {
const newItems = [...items];
if (sortData?.sortBy) {
const sortedHeader = headers.find((header) => header.key === sortData.sortBy);
if (sortedHeader?.sortFunc) {
newItems.sort((a, b) => sortedHeader.sortFunc(a, b, sortData.reverseOrder));
} else {
newItems.sort((itemA, itemB) => defaultSortCallback(itemA, itemB, sortData));
}
}
return newItems;
}, [items, sortData]);

const paginatedItems = useMemo(() => {
const start = (page - 1) * perPage;
const end = start + perPage;
return sortedItems.slice(start, end);
}, [sortedItems, perPage]);

useEffect(() => {
changePage(1);
}, [sortData, perPage]);

return (
<TableWrap minHeight={minHeight}>
<Box width={"100%"}>{header}</Box>
<TableContentWrap>
{items.length && !loading ? (
<TableContent cols={headers}>
{headers.map((header, i) => {
const isSortable = !!(header.sortable || header.sortFunc);
return (
<TableHeader key={i}>
{header.key && header.title && (
<TableHeaderTitleBtn
disabled={!isSortable}
onClick={() => {
const sortBy = header.key;
if (sortBy) {
if (sortData?.sortBy === sortBy && sortData.reverseOrder) setSortData(undefined);
else {
const reverseOrder = sortData?.sortBy !== sortBy ? false : !sortData?.reverseOrder;
setSortData({ sortBy, reverseOrder });
}
}
}}
active={sortData?.sortBy === header.key}
>
{isSortable && <SortBtn data={sortData} active={sortData?.sortBy === header.key} />}
<span>{header.title}</span>
</TableHeaderTitleBtn>
)}
{header.tooltip && (
<Box ml={"4px"}>
<Tooltip content={header.tooltip}>
<QuestionIcon size={"13px"} color="darkGray" />
</Tooltip>
</Box>
)}
</TableHeader>
);
})}
{paginatedItems.map((item) =>
headers.map((header, j) => {
const customContent = header.renderFunc && header.renderFunc(item);
const nativeContent = String(header.key ? item[header.key] : "");
const content = customContent || nativeContent;
return <TableItem key={j}>{content}</TableItem>;
})
)}
</TableContent>
) : loading ? (
<Flex alignItems={"center"} justifyContent={"center"} height={"100%"}>
<LoadingSpinner />
</Flex>
) : (
<Flex alignItems={"center"} justifyContent={"center"} height={"100%"}>
{emptyCard || <Text color="darkGray">No data</Text>}
</Flex>
)}
</TableContentWrap>
{maxPage > 1 && (
<TablePagWrap>
<TablePagBtn
disabled={!(page > 1)}
onClick={() => {
changePage(page - 1);
}}
>
<ArrowLeftIcon size={"16px"} color="primaryDefault" />
</TablePagBtn>
<Flex>
<Box mr={"0.4em"}>Page</Box>
<Box color={theme.colors.primaryDefault} minWidth={"10px"}>
{page}
</Box>
<Box mx={"0.4em"}>of</Box>
<Box>{maxPage}</Box>
</Flex>
<TablePagBtn
disabled={!(page < maxPage)}
onClick={() => {
changePage(page + 1);
}}
>
<ArrowLeftIcon size={"16px"} style={{ transform: "scaleX(-1)" }} color="primaryDefault" />
</TablePagBtn>
</TablePagWrap>
)}
</TableWrap>
);
}
Loading

0 comments on commit 16f8078

Please sign in to comment.