197 lines
5 KiB
TypeScript
197 lines
5 KiB
TypeScript
import React, { useMemo, useRef, useState } from 'react';
|
|
import usePagination, { Offset } from 'src/hooks/usePagination';
|
|
import { SelectFilter, NumberFilter, resolveFilterByType } from './Filters';
|
|
import type { DataItem, Header, Value, Filter } from './types';
|
|
import { HeaderType } from './types';
|
|
import styles from './Table.module.css';
|
|
|
|
interface Props {
|
|
data: DataItem[];
|
|
headers: Header[];
|
|
}
|
|
|
|
export default function Table({ data, headers }: Props): JSX.Element {
|
|
const [filters, setFilters] = useState<Record<string, Filter>>({});
|
|
const filtersId = useRef(crypto.randomUUID());
|
|
|
|
const filteredItems = useMemo(() => {
|
|
return data.filter((item) => {
|
|
return Object.entries(filters).every(([key, filter]) =>
|
|
resolveFilterByType(filter, item[key]),
|
|
);
|
|
});
|
|
}, [data, filters]);
|
|
|
|
const { items, changeOffset, getPaginationRange } = usePagination<DataItem>({
|
|
items: filteredItems,
|
|
limit: 10,
|
|
});
|
|
|
|
const handleUpdateFilters = (
|
|
name: string,
|
|
type: HeaderType,
|
|
value: Value,
|
|
): void => {
|
|
setFilters((prev) => {
|
|
if (value === null) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete prev[name];
|
|
return { ...prev };
|
|
}
|
|
|
|
return { ...prev, [name]: { value, type } };
|
|
});
|
|
};
|
|
|
|
function formatCell(data: DataItem, header: Header): JSX.Element {
|
|
// This formatting is only used because the source is trusted (private markdown files manage only by me)
|
|
// and because Astro don't allow me to pass JSX from an Astro file to a TSX file,
|
|
// so I have to pass the formatted row as a string.
|
|
// DON'T use this method on a public API
|
|
if (header.hasCustomCell && header.formatter) {
|
|
return (
|
|
<div dangerouslySetInnerHTML={{ __html: header.formatter(data) }} />
|
|
);
|
|
}
|
|
|
|
if (header.type === HeaderType.Multiple) {
|
|
return (
|
|
<ul className="text-start">
|
|
{data[header.key].map((item: JSX.Element, idx: number) => (
|
|
<li key={idx}>{item}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
return data[header.key];
|
|
}
|
|
|
|
function formatFilter(header: Header): JSX.Element {
|
|
const baseProps = {
|
|
key: header.key + filtersId.current,
|
|
keyData: header.key,
|
|
value: filters[header.key]?.value,
|
|
onChange: (value: Value) => {
|
|
handleUpdateFilters(header.key, header.type, value);
|
|
},
|
|
};
|
|
|
|
switch (header.type) {
|
|
case HeaderType.String:
|
|
return (
|
|
<input
|
|
onChange={(e) => {
|
|
baseProps.onChange(e.target.value);
|
|
}}
|
|
key={baseProps.key}
|
|
/>
|
|
);
|
|
case HeaderType.Number:
|
|
return (
|
|
<NumberFilter
|
|
{...baseProps}
|
|
onChange={(value: [string, number | null]) => {
|
|
handleUpdateFilters(header.key, header.type, value as Value);
|
|
}}
|
|
/>
|
|
);
|
|
case HeaderType.Select:
|
|
return <SelectFilter data={data} {...baseProps} />;
|
|
case HeaderType.Multiple:
|
|
return <SelectFilter {...baseProps} isMultiple data={data} />;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return <></>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<section className="hstack">
|
|
<button
|
|
className="ml-auto"
|
|
onClick={() => {
|
|
setFilters({});
|
|
filtersId.current = crypto.randomUUID();
|
|
}}
|
|
>
|
|
Clear Filters
|
|
</button>
|
|
</section>
|
|
|
|
<section className="mt-1 overflow-scroll">
|
|
<table className={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
{headers.map((item, idx) => (
|
|
<th key={idx}>
|
|
<div className="vstack">
|
|
{item.header}
|
|
{formatFilter(item)}
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
{items.map((item, idx) => (
|
|
<tr key={idx}>
|
|
{headers.map((header, hidx) => (
|
|
<td key={hidx}>{formatCell(item, header)}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section className="mt-1">
|
|
<button
|
|
onClick={() => {
|
|
changeOffset(Offset.First);
|
|
}}
|
|
>
|
|
First
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
changeOffset(Offset.Prev);
|
|
}}
|
|
>
|
|
Prev
|
|
</button>
|
|
|
|
{getPaginationRange().map((item) => (
|
|
<button
|
|
className={item.current ? 'btn-primary' : ''}
|
|
key={item.page}
|
|
onClick={() => {
|
|
changeOffset(Offset.To, item.page);
|
|
}}
|
|
>
|
|
{item.page}
|
|
</button>
|
|
))}
|
|
|
|
<button
|
|
onClick={() => {
|
|
changeOffset(Offset.Next);
|
|
}}
|
|
>
|
|
Next
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
changeOffset(Offset.Last);
|
|
}}
|
|
>
|
|
Last
|
|
</button>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|