table

This commit is contained in:
Max S 2024-07-16 16:33:41 +02:00
parent 9cb87460c1
commit 3b965966cb
13 changed files with 445 additions and 229 deletions

19
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@types/node": "18.15.1",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@uidotdev/usehooks": "^2.4.1",
"class-validator": "^0.14.0",
"classnames": "^2.3.2",
"crypto-random-string": "^5.0.0",
@ -23,6 +24,7 @@
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"form-data": "^4.0.0",
"heroicons": "^2.1.5",
"jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151",
"next": "^14.2.3",
@ -1153,6 +1155,18 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@uidotdev/usehooks": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
"integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@ -3174,6 +3188,11 @@
"node": ">= 0.4"
}
},
"node_modules/heroicons": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/heroicons/-/heroicons-2.1.5.tgz",
"integrity": "sha512-XLq3m45bJphmWdR6im52alaYajp0/fluJa2+7xh3x7CgItumbLsjhKYe+mCf0lErXLy7ZyiEgKIty2gFNxhoyA=="
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",

View File

@ -18,6 +18,7 @@
"@types/node": "18.15.1",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@uidotdev/usehooks": "^2.4.1",
"class-validator": "^0.14.0",
"classnames": "^2.3.2",
"crypto-random-string": "^5.0.0",
@ -25,6 +26,7 @@
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"form-data": "^4.0.0",
"heroicons": "^2.1.5",
"jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151",
"next": "^14.2.3",

View File

@ -0,0 +1,40 @@
.root {
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
overflow: hidden;
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
svg {
cursor: pointer;
min-width: 21px;
fill: var(--color-neutral-600);
}
}
.cell {
overflow: hidden;
word-wrap: break-word;
.content {
max-width: 270px;
width: 100%;
word-break: break-word;
> :first-child {
width: 100%;
}
}
&:hover {
background-color: var(--background-elevation-1);
}
}
}

View File

@ -0,0 +1,101 @@
import InfiniteScroll from "@Front/Components/Elements/InfiniteScroll";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography, { ETypo, ETypoColor } from "../../Typography";
import classes from "./classes.module.scss";
export type IRowProps = { key: string } & Record<string, React.ReactNode>;
type IRow = {
key?: string;
content: Record<string, CellContent>;
};
type IProps = {
header: readonly IHead[];
rows: IRowProps[];
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
};
export type IHead = {
key: string;
title?: string;
};
type CellContent = {
key: string;
value: React.ReactNode;
};
export default function MuiTable(props: IProps) {
const rows: IRow[] = props.rows.map((rowProps) => {
const row: IRow = {
key: rowProps.key,
content: {},
};
props.header.forEach((column) => {
const cellContent: CellContent = {
key: column.key + rowProps.key,
value: rowProps[column.key],
};
row.content[column.key] = cellContent;
});
return row;
});
return (
<InfiniteScroll orientation="vertical" onNext={props.onNext} offset={0}>
<TableContainer className={classes["root"]} sx={{ maxHeight: "80vh", overflowY: "auto", overflowX: "hidden" }}>
<Table aria-label="simple table" sx={{ border: "0" }}>
<TableHead sx={{ position: "sticky", top: "0", borderBottom: "1px solid var(--color-neutral-200)" }}>
<TableRow>
{props.header.map((column) => (
<TableCell key={column.key} align={"left"} sx={{ border: 0 }}>
{column.title && (
<span className={classes["head"]}>
<Typography
className={classes["text"]}
typo={ETypo.TEXT_SM_REGULAR}
color={ETypoColor.COLOR_NEUTRAL_600}>
{column.title}
</Typography>
<ChevronDownIcon width={21} />
</span>
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => {
return (
<TableRow key={row.key} sx={{ verticalAlign: "middle" }}>
{Object.values(row.content).map((cell) => (
<TableCell
className={classes["cell"]}
key={cell.key}
align="left"
sx={{ border: 0, padding: "4px 8px", height: "53px" }}>
<Typography
className={classes["content"]}
typo={ETypo.TEXT_MD_REGULAR}
color={ETypoColor.COLOR_NEUTRAL_900}>
{cell.value}
</Typography>
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</InfiniteScroll>
);
}

View File

@ -0,0 +1,40 @@
.root {
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
width: 100%;
border: 1px solid var(--Wild-Sand-100, #efefef);
background-color: var(--Wild-Sand-50, #f6f6f6);
box-sizing: border-box;
.input-element {
padding: 10.5px 16px;
flex: 1;
border: 0;
background-color: transparent;
&:focus,
input:focus {
outline: none;
}
&::placeholder {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
}
.icon {
position: absolute;
right: 10px;
width: 24px;
height: 24px;
pointer-events: none;
path {
stroke: var(--Wild-Sand-600, #bfbfbf);
}
}
}

View File

@ -0,0 +1,33 @@
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import classes from "./classes.module.scss";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useDebounce } from "@uidotdev/usehooks";
type IProps = {
onChange?: (search: string) => void;
placeholder: string;
};
export default function SearchBar(props: IProps) {
const { placeholder, onChange } = props;
const [search, setSearch] = useState<string | null>(null);
const debouncedSearch = useDebounce(search, 200);
const onSearch = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.currentTarget.value);
}, []);
useEffect(() => {
if (debouncedSearch === null) return;
onChange?.(debouncedSearch);
}, [debouncedSearch, onChange]);
return (
<div className={classes["root"]}>
<input className={classes["input-element"]} onChange={onSearch} type="text" placeholder={placeholder} />
<MagnifyingGlassIcon className={classes["icon"]} />
</div>
);
}

View File

@ -0,0 +1,13 @@
.root {
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
.input-container {
width: 300px;
cursor: text;
}
}
}

View File

@ -0,0 +1,45 @@
import classNames from "classnames";
import MuiTable, { IRowProps, IHead } from "./MuiTable";
import SearchBarTable from "./SearchBar";
import classes from "./classes.module.scss";
import { useCallback, useRef } from "react";
type IProps = {
header: readonly IHead[];
rows: IRowProps[];
count?: number;
className?: string;
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
searchBar?: {
placeholder?: string;
onSearch?: (search: string) => void;
};
};
export default function Table(props: IProps) {
const { className, header, rows, searchBar } = props;
const keyId = useRef<number>(0);
const onSearch = useCallback(
(search: string) => {
keyId.current++;
searchBar?.onSearch?.(search);
},
[searchBar],
);
return (
<div className={classNames(classes["root"], className)}>
{searchBar && (
<div className={classes["header"]}>
<div>{props.count ?? rows.length} resultats</div>
<div className={classes["input-container"]}>
<SearchBarTable onChange={onSearch} placeholder={searchBar.placeholder ?? ""} />
</div>
</div>
)}
<MuiTable key={keyId.current} header={header} rows={rows} onNext={props.onNext} />
</div>
);
}

View File

@ -6,6 +6,7 @@
display: flex;
align-items: center;
justify-content: center;
&.info {
background-color: var(--color-info-50);

View File

@ -1,62 +0,0 @@
import React from "react";
import classes from "./old-classes.module.scss";
import classNames from "classnames";
type IProps = {
typo: ITypo;
children: React.ReactNode;
color?: ITypoColor;
className?: string;
title?: string;
};
type IState = {};
export enum ITypo {
H1 = "H1-60",
H1Bis = "H1-bis-40",
H2 = "H2-30",
H3 = "H3-24",
P_SB_18 = "Paragraphe-semibold-18",
P_18 = "Paragraphe-simple-18",
P_MAJ_18 = "Paragraphe-MAJ-18",
NAV_HEADER_18 = "Nav-header-off-18",
P_ERR_18 = "Paragraphe-18-error",
P_SB_16 = "Paragraphe-semibold-16",
P_16 = "Paragraphe-simple-16",
NAV_INPUT_16 = "Nav-input-off-16",
P_ERR_16 = "Paragraphe-16-error",
CAPTION_14 = "Caption_14",
CAPTION_14_SB = "Caption_14-semibold",
}
export enum ITypoColor {
COLOR_ERROR_800 = "color-error-800",
COLOR_NEUTRAL_500 = "color-neutral-500",
COLOR_GENERIC_BLACK = "color-generic-black",
COLOR_PRIMARY_500 = "color-primary-500",
COLOR_SECONDARY_500 = "color-secondary-500",
COLOR_SUCCESS_600 = "color-success-600",
COLOR_WARNING_500 = "color-warning-500",
COLOR_ERROR_600 = "color-error-600",
COLOR_GENERIC_WHITE = "color-generic-white",
}
export default class Typography extends React.Component<IProps, IState> {
public override render(): JSX.Element {
return (
<div
className={classNames(
classes["root"],
classes[this.props.typo],
classes[this.props.color ?? ""],
this.props.className ?? "",
)}
title={this.props.title}>
{this.props.children}
</div>
);
}
}

View File

@ -1,166 +0,0 @@
@import "@Themes/constants.scss";
@import "@Themes/modes.scss";
.root {
color: var(--color-generic-black);
vertical-align: center;
font-family: "Inter", sans-serif;
&.H1-60 {
font-style: normal;
font-weight: 500;
font-size: 56px;
line-height: 67.2px;
@media (max-width: $screen-m) {
font-size: 48px;
line-height: 56.7px;
}
}
&.H1-bis-40 {
font-style: normal;
font-weight: 500;
font-size: 40px;
line-height: 48px;
}
&.H2-30 {
font-style: normal;
font-weight: 500;
font-size: 30px;
line-height: 36px;
}
&.H3-24 {
font-style: normal;
font-weight: 600;
font-size: 24px;
line-height: 29px;
}
&.Paragraphe-semibold-18 {
font-style: normal;
font-weight: 600;
font-size: 18px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.Paragraphe-simple-18 {
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
}
&.Paragraphe-MAJ-18 {
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
text-transform: uppercase;
}
&.Nav-header-off-18 {
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
letter-spacing: 0.5px;
color: var(--color-neutral-500);
}
&.Paragraphe-18-error {
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.Paragraphe-semibold-16 {
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.Nav-input-off-16 {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0.5px;
color: var(--color-neutral-500);
}
&.Paragraphe-simple-16 {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0.005em;
}
&.Paragraphe-16-error {
color: var(--color-error-800;
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.Caption_14 {
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.Caption_14-semibold {
font-style: normal;
font-weight: 600;
font-size: 14px;
line-height: 22px;
letter-spacing: 0.5px;
}
&.color-error-800 {
color: var(--color-error-800;
}
&.color-neutral-500 {
color: var(--color-neutral-500);
}
&.color-generic-black {
color: var(--color-generic-black);
}
&.color-primary-500 {
color: var(--color-primary-500);
}
&.color-secondary-500 {
color: var(--color-secondary-500);
}
&.color-success-600 {
color: var(--color-success-600);
}
&.color-error-600 {
color: var(--color-error-600);
}
&.color-warning-500 {
color: var(--color-warning-500);
}
&.white {
color: var(--color-generic-white);
}
}

View File

@ -0,0 +1,105 @@
import React from "react";
/**
* @Documentation
* This component is used to create an infinite scroll effect.
*
* Usage:
*
* No pagination: front-end/src/components/pages/Products/index.tsx
const onSearch = useCallback((searchParam: string) => productService.search(searchParam).then((products) => setRows(buildRows(products))), []);
useEffect(() => {
productService.getLastProductSheets().then((products) => setRows(buildRows(products)));
}, []);
*
*
*
* With pagination: front-end/src/components/pages/Clients/index.tsx
*
const fetchClients = useCallback(
() =>
clientService.get(pagination.current, search.current).then((clients) => {
if (clients.length === 0) return [];
setRows((_rows) => [..._rows, ...buildRows(clients)]);
pagination.current.skip += pagination.current.take;
return clients;
}),
[],
);
const onNext = useCallback(
(release: () => void) => {
fetchClients().then((clients) => {
if (!clients.length) return console.warn("No more value to load");
release();
});
},
[fetchClients],
);
const onSearch = useCallback((searchParam: string) => {
pagination.current.skip = 0;
search.current = (searchParam && searchParam.trim()) || null;
setRows([]);
}, []);
*
*/
export type IPagination = {
take: number;
skip: number;
};
type IProps = {
offset?: number;
orientation?: "vertical" | "horizontal";
/**
* @description
* If `onNext` is set to `null`, it indicates that there is no pagination and the infinite scroll effect will not be triggered.
*/
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
children: React.ReactElement;
};
export default function InfiniteScroll({ children, onNext, offset = 20, orientation = "vertical" }: IProps) {
const isWaiting = React.useRef<boolean>(false);
const elementRef = React.useRef<HTMLElement>();
const onChange = React.useCallback(() => {
if (!onNext) return;
const element = elementRef.current;
if (!element || isWaiting.current) return;
const { scrollTop, scrollLeft, clientHeight, clientWidth, scrollHeight, scrollWidth } = element;
let isChange = false;
if (orientation === "vertical") isChange = scrollTop + clientHeight >= scrollHeight - offset;
if (orientation === "horizontal") isChange = scrollLeft + clientWidth >= scrollWidth - offset;
if (isChange) {
isWaiting.current = true;
onNext(() => (isWaiting.current = false));
}
}, [onNext, offset, orientation]);
React.useEffect(() => onChange(), [onChange]);
React.useEffect(() => {
const observer = new MutationObserver(onChange);
elementRef.current && observer.observe(elementRef.current, { childList: true, subtree: true });
window.addEventListener("resize", onChange);
return () => {
observer.disconnect();
window.removeEventListener("resize", onChange);
};
}, [onChange]);
if (!onNext) return children;
const clonedChild = React.cloneElement(children, {
onScroll: onChange,
ref: elementRef,
});
return clonedChild;
}

View File

@ -5,9 +5,10 @@ import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import BasePage from "../Base";
import classes from "./classes.module.scss";
import Newletter from "@Front/Components/DesignSystem/Newsletter";
import Button, { EButtonStyleType, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Button, { EButtonSize, EButtonStyleType, EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
import Table from "@Front/Components/DesignSystem/Table";
type IProps = {};
type IState = {
@ -52,6 +53,50 @@ export default class Folder extends BasePage<IProps, IState> {
<Tag color={ETagColor.WARNING} variant={ETagVariant.SEMI_BOLD} label="WARNING" />
<Tag color={ETagColor.ERROR} variant={ETagVariant.SEMI_BOLD} label="ERROR" />
</div>
<Table
header={[
{
key: "name",
title: "Nom",
},
{
key: "firstname",
title: "Prénom",
},
{
key: "button",
},
]}
rows={[
{
key: "1",
name: "Doe",
firstname: "John",
button: (
<Button size={EButtonSize.SM} variant={EButtonVariant.PRIMARY}>
Primary
</Button>
),
},
{
key: "2",
name: "Doe",
firstname: "Jane",
button: <Tag color={ETagColor.SUCCESS} variant={ETagVariant.SEMI_BOLD} label="Info" />,
},
{
key: "3",
name: "Doe",
firstname: "Jack",
button: (
<Button size={EButtonSize.SM} variant={EButtonVariant.NEUTRAL}>
Neutral
</Button>
),
},
]}
/>
</div>
<div className={classes["no-folder-selected"]}>