klioso
Version:
Multi-Service Provider Management System for WordPress and Web Development with Enhanced Scanner
172 lines (161 loc) • 9.03 kB
JSX
import React from 'react';
// Simple SVG icons to avoid dependency conflicts
const ChevronUpIcon = ({ className }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
);
const ChevronDownIcon = ({ className }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
);
export default function ResponsiveTable({
columns = [],
data = [],
loading = false,
onRowClick,
sortBy = null,
sortDirection = 'asc',
onSort = null
}) {
const getSortIcon = (columnKey) => {
if (sortBy !== columnKey) {
return (
<div className="flex flex-col opacity-30">
<ChevronUpIcon className="h-3 w-3 -mb-1" />
<ChevronDownIcon className="h-3 w-3" />
</div>
);
}
return sortDirection === 'asc' ?
<ChevronUpIcon className="h-4 w-4" /> :
<ChevronDownIcon className="h-4 w-4" />;
};
const handleHeaderClick = (column) => {
if (column.sortable !== false && onSort) {
onSort(column.key);
}
};
return (
<>
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{columns.map((col, idx) => (
<th
key={col?.key || col?.label || `header-${idx}`}
className={`px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ${
col.sortable !== false && onSort ? 'cursor-pointer hover:bg-gray-100' : ''
}`}
onClick={() => handleHeaderClick(col)}
>
<div className="flex items-center space-x-1">
<span>{col?.label || ''}</span>
{col.sortable !== false && onSort && (
<span className="ml-1">
{getSortIcon(col.key)}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={columns.length} className="px-6 py-8 text-center text-gray-400">
<div className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</div>
</td>
</tr>
) : (!data || data.length === 0) ? (
<tr>
<td colSpan={columns.length} className="px-6 py-8 text-center text-gray-400">
No records found.
</td>
</tr>
) : (
data.map((row, idx) => (
<tr
key={row?.id || `row-${idx}`}
className={`${onRowClick ? "cursor-pointer hover:bg-gray-50" : ""} transition-colors duration-150`}
onClick={onRowClick ? () => onRowClick(row) : undefined}
>
{columns.map((col, colIdx) => (
<td key={col?.key || col?.label || `col-${colIdx}`} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{col.render ? col.render(row) : row?.[col.key]}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Mobile Card View */}
<div className="md:hidden">
{loading ? (
<div className="p-8 text-center text-gray-400">
<div className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</div>
</div>
) : (!data || data.length === 0) ? (
<div className="p-8 text-center text-gray-400">
No records found.
</div>
) : (
<div className="divide-y divide-gray-200">
{data.map((row, idx) => (
<div
key={row?.id || `card-${idx}`}
className={`p-4 ${onRowClick ? "cursor-pointer hover:bg-gray-50" : ""} transition-colors duration-150`}
onClick={onRowClick ? () => onRowClick(row) : undefined}
>
<div className="space-y-2">
{columns.map((col, colIdx) => {
// Skip actions column or columns marked as hidden on mobile
if (col.key === 'actions' || col.hideOnMobile) {
return null;
}
const value = col.render ? col.render(row) : row?.[col.key];
if (!value || value === '-') return null;
return (
<div key={col?.key || col?.label || `mobile-col-${colIdx}`} className="flex justify-between items-start">
<span className="text-sm font-medium text-gray-500 w-1/3 flex-shrink-0">
{col?.label}:
</span>
<span className="text-sm text-gray-900 w-2/3 text-right">
{value}
</span>
</div>
);
})}
{/* Always show actions at the bottom of mobile cards */}
{columns.find(col => col.key === 'actions') && (
<div className="pt-2 border-t border-gray-100 mt-3">
{columns.find(col => col.key === 'actions').render(row)}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</>
);
}