chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
+254
View File
@@ -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;