Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"framer-motion": "^6.3.3",
"lodash": "^4.17.21",
"luxon": "^1.25.0",
"nuqs": "^1.17.1",
"notistack": "^2.0.4",
"react": "18.2.0",
"react-chartjs-2": "^2.11.1",
Expand All @@ -61,7 +62,8 @@
"react-modal": "^3.15.1",
"react-quill": "2.0.0-beta.4",
"regenerator-runtime": "0.13.7",
"yup": "^0.32.9"
"yup": "^0.32.9",
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/core": "^7.15.5",
Expand Down
162 changes: 162 additions & 0 deletions packages/ui-components/src/lib/data-table/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { FC, ReactNode, useMemo } from 'react';
import classNames from 'classnames';
import { z } from 'zod';
import { useQueryState } from 'nuqs';
import { Table, TableProps } from '../table';
import { Pagination } from '../pagination';

import './data-table.css';

export interface DataTableColumn<T> {
header: string;
accessor: keyof T;
render?: (value: any, row: T) => ReactNode;
}

export interface DataTableFilter<T> {
name: string;
label: string;
schema: z.ZodType<any>;
component: FC<{
value: any;
onChange: (value: any) => void;
label: string;
}>;
defaultValue?: any;
}

export interface DataTableProps<T> extends Omit<TableProps, 'children'> {
data: T[];
columns: DataTableColumn<T>[];
filters?: DataTableFilter<T>[];
itemsPerPage?: number;
onFilterChange?: (filters: Record<string, any>) => void;
}

export function DataTable<T>({
data,
columns,
filters = [],
className,
itemsPerPage = 10,
onFilterChange,
...props
}: DataTableProps<T>) {
// Set up filter state with nuqs
const filterStates = useMemo(() => {
return filters.map(filter => {
const [value, setValue] = useQueryState(
filter.name,
{
defaultValue: filter.defaultValue ?? null,
parse: (value) => {
try {
const parsed = filter.schema.parse(JSON.parse(value));
return parsed;
} catch (e) {
return filter.defaultValue ?? null;
}
},
serialize: (value) => {
return JSON.stringify(value);
}
}
);

return { filter, value, setValue };
});
}, [filters]);

// Set up pagination
const [page, setPage] = useQueryState('page', {
defaultValue: 1,
parse: (value) => {
const parsed = parseInt(value, 10);
return isNaN(parsed) || parsed < 1 ? 1 : parsed;
},
serialize: (value) => value.toString()
});

// Apply filters to data
const filteredData = useMemo(() => {
let result = [...data];

// Apply each active filter
const activeFilters: Record<string, any> = {};

filterStates.forEach(({ filter, value }) => {
if (value !== null && value !== undefined) {
activeFilters[filter.name] = value;
}
});

// Notify parent component about filter changes
if (onFilterChange) {
onFilterChange(activeFilters);
}

return result;
}, [data, filterStates, onFilterChange]);

// Paginate data
const paginatedData = useMemo(() => {
const startIndex = (page - 1) * itemsPerPage;
return filteredData.slice(startIndex, startIndex + itemsPerPage);
}, [filteredData, page, itemsPerPage]);

// Calculate total pages
const totalPages = Math.ceil(filteredData.length / itemsPerPage);

return (
<div className={classNames('lc-data-table', className)}>
{filters.length > 0 && (
<div className="lc-data-table-filters">
{filterStates.map(({ filter, value, setValue }) => (
<div key={filter.name} className="lc-data-table-filter">
<filter.component
value={value}
onChange={setValue}
label={filter.label}
/>
</div>
))}
</div>
)}

<div className="lc-data-table-wrapper">
<Table {...props}>
<thead>
<tr>
{columns.map((column) => (
<th key={column.accessor as string}>{column.header}</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((column) => (
<td key={`${rowIndex}-${column.accessor as string}`}>
{column.render
? column.render(row[column.accessor], row)
: row[column.accessor]}
</td>
))}
</tr>
))}
</tbody>
</Table>
</div>

{totalPages > 1 && (
<div className="lc-data-table-pagination">
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={setPage}
/>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { FC } from 'react';
import { DataTable } from '..';
import { createTextFilter, createSelectFilter, createNumberFilter, createDateFilter } from '../filters';

interface User {
id: number;
name: string;
email: string;
role: string;
age: number;
joinDate: string;
}

const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin', age: 32, joinDate: '2022-01-15' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'Editor', age: 28, joinDate: '2022-03-22' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'User', age: 45, joinDate: '2021-11-05' },
{ id: 4, name: 'Alice Brown', email: '[email protected]', role: 'Admin', age: 37, joinDate: '2022-02-18' },
{ id: 5, name: 'Charlie Wilson', email: '[email protected]', role: 'User', age: 29, joinDate: '2022-04-10' },
{ id: 6, name: 'Diana Miller', email: '[email protected]', role: 'Editor', age: 41, joinDate: '2021-10-30' },
{ id: 7, name: 'Edward Davis', email: '[email protected]', role: 'User', age: 33, joinDate: '2022-05-05' },
{ id: 8, name: 'Fiona Clark', email: '[email protected]', role: 'Admin', age: 39, joinDate: '2021-12-12' },
{ id: 9, name: 'George White', email: '[email protected]', role: 'User', age: 26, joinDate: '2022-06-20' },
{ id: 10, name: 'Hannah Lee', email: '[email protected]', role: 'Editor', age: 31, joinDate: '2022-01-25' },
{ id: 11, name: 'Ian Taylor', email: '[email protected]', role: 'User', age: 44, joinDate: '2021-09-15' },
{ id: 12, name: 'Julia Martin', email: '[email protected]', role: 'Admin', age: 35, joinDate: '2022-03-05' }
];

export const DataTableExample: FC = () => {
const columns = [
{ header: 'ID', accessor: 'id' as keyof User },
{ header: 'Name', accessor: 'name' as keyof User },
{ header: 'Email', accessor: 'email' as keyof User },
{ header: 'Role', accessor: 'role' as keyof User },
{ header: 'Age', accessor: 'age' as keyof User },
{ header: 'Join Date', accessor: 'joinDate' as keyof User }
];

const filters = [
createTextFilter('name', 'Name', 'Search by name...'),
createSelectFilter('role', 'Role', [
{ value: 'Admin', label: 'Admin' },
{ value: 'Editor', label: 'Editor' },
{ value: 'User', label: 'User' }
]),
createNumberFilter('age', 'Age', { min: 18, max: 100 }),
createDateFilter('joinDate', 'Join Date')
];

return (
<DataTable
data={mockUsers}
columns={columns}
filters={filters}
itemsPerPage={5}
footnote="This is an example of a data table with filtering and pagination."
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { DataTable } from '..';
import { DataTableExample } from './data-table.examples';

<Meta title="Components/DataTable" component={DataTable} />

# DataTable

The DataTable component is a powerful table component that supports filtering, pagination, and sorting.
It uses `nuqs` for URL-based state management and `zod` for schema validation of filter values.

## Features

- URL-based filtering with `nuqs`
- Schema validation with `zod`
- Pagination
- Custom filter components
- Responsive design

## Basic Usage

<Canvas>
<Story name="Default">
<DataTableExample />
</Story>
</Canvas>

## Props

<ArgsTable of={DataTable} />

## Filter Types

The DataTable comes with several built-in filter types:

- **Text Filter**: For filtering text fields
- **Select Filter**: For selecting from predefined options
- **Number Filter**: For filtering numeric values
- **Date Filter**: For filtering date values

Each filter type has a corresponding schema defined with `zod` for validation.

## Creating Custom Filters

You can create custom filters by implementing the `DataTableFilter` interface:

```tsx
import { z } from 'zod';

const myCustomFilter = {
name: 'myFilter',
label: 'My Filter',
schema: z.string().nullable(),
component: ({ value, onChange, label }) => (
<MyCustomComponent
value={value}
onChange={onChange}
label={label}
/>
),
defaultValue: null
};
```

## URL State Management

The DataTable uses `nuqs` to manage filter state in the URL. This allows for:

- Shareable filtered views
- Browser history navigation
- Persistence of filters across page reloads

## Example Implementation

```tsx
import { DataTable } from '@lambdacurry/component-library';
import { createTextFilter, createSelectFilter } from '@lambdacurry/component-library';

const MyDataTable = () => {
const data = [...]; // Your data array

const columns = [
{ header: 'Name', accessor: 'name' },
{ header: 'Email', accessor: 'email' },
{ header: 'Role', accessor: 'role' }
];

const filters = [
createTextFilter('name', 'Name', 'Search by name...'),
createSelectFilter('role', 'Role', [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' }
])
];

return (
<DataTable
data={data}
columns={columns}
filters={filters}
itemsPerPage={10}
/>
);
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './data-table.examples';
23 changes: 23 additions & 0 deletions packages/ui-components/src/lib/data-table/data-table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.lc-data-table {
&-wrapper {
width: 100%;
overflow-x: auto;
}

&-filters {
display: flex;
flex-wrap: wrap;
gap: theme('spacing.16');
margin-bottom: theme('spacing.16');
}

&-filter {
min-width: 200px;
}

&-pagination {
margin-top: theme('spacing.16');
display: flex;
justify-content: flex-end;
}
}
Loading
Loading