chore: import codes
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Badge from './Badge';
|
||||
import { TorriGateIcon } from '../Icons';
|
||||
|
||||
const ROW_SIZE = {
|
||||
sm: { cell: 'px-4 py-3', header: 'px-6 py-4', mobile: 'p-4' },
|
||||
md: { cell: 'px-6 py-4', header: 'px-6 py-4', mobile: 'p-6' },
|
||||
lg: { cell: 'px-6 py-5', header: 'px-6 py-5', mobile: 'p-8' },
|
||||
};
|
||||
|
||||
const Table = ({
|
||||
columns = [],
|
||||
data = [],
|
||||
loading = false,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onSort,
|
||||
onRowClick,
|
||||
getRowProps,
|
||||
emptyMessage = 'Aucune donnée',
|
||||
emptyDescription = 'Aucun élément à afficher',
|
||||
size = 'md',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const sizeClasses = ROW_SIZE[size] ?? ROW_SIZE.md;
|
||||
const SortIcon = ({ column }) => {
|
||||
const isActive = sortBy === column.key;
|
||||
const isDesc = isActive && sortOrder === 'desc';
|
||||
|
||||
return (
|
||||
<span className="ml-1">
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${isActive ? (isDesc ? 'rotate-180' : '') : 'text-neutral-500 dark:text-neutral-400'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Skeleton = ({ className = "", width = "100%", height = "h-4" }) => (
|
||||
<div
|
||||
className={`bg-gradient-to-r from-neutral-200 via-neutral-100 to-neutral-200 dark:from-neutral-700/30 dark:via-neutral-600/30 dark:to-neutral-700/30 bg-[length:200%_100%] animate-shimmer rounded ${height} ${className}`}
|
||||
style={{ width }}
|
||||
/>
|
||||
);
|
||||
|
||||
const SkeletonRow = () => (
|
||||
<tr className="hover:bg-neutral-100 dark:hover:bg-neutral-700/20 transition-colors">
|
||||
{columns.map((column, index) => (
|
||||
<td key={index} className={`${sizeClasses.cell} ${column.noWrap !== false ? '' : 'whitespace-nowrap'}`}>
|
||||
{column.skeleton ? (
|
||||
column.skeleton.secondary ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton
|
||||
className={column.skeleton.className}
|
||||
height={column.skeleton.height}
|
||||
width={column.skeleton.width}
|
||||
/>
|
||||
<Skeleton
|
||||
height={column.skeleton.secondary.height}
|
||||
width={column.skeleton.secondary.width}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton
|
||||
className={column.skeleton.className}
|
||||
height={column.skeleton.height}
|
||||
width={column.skeleton.width}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Skeleton height="h-4" width="60%" />
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
||||
const EmptyState = () => (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-6 py-12 text-center">
|
||||
<div className="text-neutral-500 dark:text-neutral-400 mb-2">
|
||||
<TorriGateIcon className="mx-auto h-12 w-12 mb-4" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1">{emptyMessage}</p>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">{emptyDescription}</p>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const renderCellContent = (item, column) => {
|
||||
if (column.render) {
|
||||
return column.render(item);
|
||||
}
|
||||
|
||||
const value = column.key.split('.').reduce((obj, key) => obj?.[key], item);
|
||||
|
||||
if (column.type === 'badge') {
|
||||
return <Badge variant={column.badgeVariant}>{value}</Badge>;
|
||||
}
|
||||
|
||||
if (column.type === 'date') {
|
||||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
|
||||
if (column.type === 'currency') {
|
||||
return `$${parseFloat(value || 0).toFixed(2)}`;
|
||||
}
|
||||
|
||||
return value || '-';
|
||||
};
|
||||
|
||||
const MobileCard = ({ item }) => (
|
||||
<div className={`${sizeClasses.mobile} space-y-3`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{columns.slice(0, 2).map((column) => (
|
||||
<div key={column.key} className={column.key === columns[0]?.key ? 'mb-2' : ''}>
|
||||
{renderCellContent(item, column)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 ml-4">
|
||||
{columns.slice(2, 4).map((column) => (
|
||||
<div key={column.key}>
|
||||
{renderCellContent(item, column)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{columns.length > 4 && (
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{columns.slice(4).map((column) => (
|
||||
<div key={column.key} className="mb-1">
|
||||
<span className="font-medium">{column.label}:</span> {renderCellContent(item, column)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MobileSkeletonCard = () => (
|
||||
<div className={`${sizeClasses.mobile} space-y-3`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton height="h-4" width="70%" />
|
||||
<Skeleton height="h-3" width="40%" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 ml-4">
|
||||
<Skeleton className="rounded-full" height="h-6" width="80px" />
|
||||
<Skeleton className="rounded-full" height="h-6" width="90px" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton height="h-3" width="50%" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden lg:block overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-neutral-200 dark:border-neutral-700/30">
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`${sizeClasses.header} ${column.headerAlign === 'right' ? 'text-right' : 'text-left'} text-xs font-medium text-neutral-600 dark:text-neutral-300 uppercase tracking-wider ${
|
||||
column.sortable ? 'cursor-pointer hover:text-neutral-900 dark:hover:text-white transition-colors' : ''
|
||||
}`}
|
||||
onClick={column.sortable && onSort ? () => onSort(column.key) : undefined}
|
||||
>
|
||||
<div className={`flex items-center ${column.headerAlign === 'right' ? 'justify-end' : ''}`}>
|
||||
{column.label}
|
||||
{column.sortable && onSort && <SortIcon column={column} />}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-700/30">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<SkeletonRow key={index} />
|
||||
))
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
data.map((item, index) => {
|
||||
const { className: extraClassName, ...rowExtraProps } = getRowProps ? getRowProps(item) : {};
|
||||
return (
|
||||
<tr
|
||||
key={item.id || index}
|
||||
className={`hover:bg-neutral-100 dark:hover:bg-neutral-700/20 transition-colors ${onRowClick ? 'cursor-pointer' : ''} ${extraClassName || ''}`}
|
||||
onClick={onRowClick ? () => onRowClick(item) : undefined}
|
||||
{...rowExtraProps}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<td key={column.key} className={`${sizeClasses.cell} ${column.noWrap !== false ? '' : 'whitespace-nowrap'}`}>
|
||||
{renderCellContent(item, column)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<div className="lg:hidden divide-y divide-neutral-200 dark:divide-neutral-700/30">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, index) => (
|
||||
<MobileSkeletonCard key={index} />
|
||||
))
|
||||
) : data.length === 0 ? (
|
||||
<div className={`${sizeClasses.mobile} py-12 text-center`}>
|
||||
<div className="text-neutral-500 dark:text-neutral-400 mb-2">
|
||||
<svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2M4 13h2m13-8V4a1 1 0 00-1-1H7a1 1 0 00-1 1v1m8 0V4.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1">{emptyMessage}</p>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">{emptyDescription}</p>
|
||||
</div>
|
||||
) : (
|
||||
data.map((item, index) => {
|
||||
const { className: extraClassName, ...rowExtraProps } = getRowProps ? getRowProps(item) : {};
|
||||
return (
|
||||
<div
|
||||
key={item.id || index}
|
||||
className={`${onRowClick ? 'cursor-pointer' : ''} ${extraClassName || ''}`}
|
||||
onClick={onRowClick ? () => onRowClick(item) : undefined}
|
||||
{...rowExtraProps}
|
||||
>
|
||||
<MobileCard item={item} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
Reference in New Issue
Block a user