Install admin-lte@4.1.0 (+ bootstrap 5.3.8, bootstrap-icons as peer deps) and rebuild every desktop component against actual AdminLTE markup pulled from its live docs/demo, rather than a Tailwind approximation: - Real app-wrapper/app-header/app-sidebar/app-main/app-footer structure - Working PushMenu (sidebar collapse), Treeview, ColorMode (light/dark/auto) - Navbar: functional search (wired to Users list ?search=), notifications dropdown (MQTT-driven), user menu dropdown - Sidebar: grouped nav-header sections, active-route highlighting - Breadcrumbs, footer, Bootstrap card/table/pagination/progress widgets Tailwind is now imported utilities-only (no preflight) so it coexists with Bootstrap's reset; mobile shell and modals are untouched and still Tailwind.
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { Card } from '../desktop/Card'
|
|
|
|
export interface DataTableColumn<T> {
|
|
key: string
|
|
label: string
|
|
sortable?: boolean
|
|
render: (row: T) => ReactNode
|
|
}
|
|
|
|
interface DataTableProps<T> {
|
|
columns: DataTableColumn<T>[]
|
|
rows: T[]
|
|
rowKey: (row: T) => string
|
|
sort?: string
|
|
direction?: 'asc' | 'desc'
|
|
onSortChange?: (key: string) => void
|
|
page: number
|
|
lastPage: number
|
|
onPageChange: (page: number) => void
|
|
isLoading?: boolean
|
|
}
|
|
|
|
export function DataTable<T>({
|
|
columns,
|
|
rows,
|
|
rowKey,
|
|
sort,
|
|
direction,
|
|
onSortChange,
|
|
page,
|
|
lastPage,
|
|
onPageChange,
|
|
isLoading,
|
|
}: DataTableProps<T>) {
|
|
const pages = Array.from({ length: lastPage }, (_, i) => i + 1)
|
|
|
|
return (
|
|
<Card>
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
{columns.map((col) => (
|
|
<th key={col.key}>
|
|
{col.sortable ? (
|
|
<button
|
|
onClick={() => onSortChange?.(col.key)}
|
|
className="btn btn-link p-0 text-decoration-none fw-semibold"
|
|
>
|
|
{col.label} {sort === col.key && <i className={`bi bi-caret-${direction === 'asc' ? 'up' : 'down'}-fill`}></i>}
|
|
</button>
|
|
) : (
|
|
col.label
|
|
)}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{isLoading ? (
|
|
<tr>
|
|
<td colSpan={columns.length} className="text-center text-secondary py-4">
|
|
Loading…
|
|
</td>
|
|
</tr>
|
|
) : rows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length} className="text-center text-secondary py-4">
|
|
No results.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
rows.map((row) => (
|
|
<tr key={rowKey(row)}>
|
|
{columns.map((col) => (
|
|
<td key={col.key}>{col.render(row)}</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{lastPage > 1 && (
|
|
<nav className="mt-3" aria-label="Table pagination">
|
|
<ul className="pagination pagination-sm justify-content-end mb-0">
|
|
<li className={`page-item ${page <= 1 ? 'disabled' : ''}`}>
|
|
<button className="page-link" onClick={() => onPageChange(page - 1)}>
|
|
Previous
|
|
</button>
|
|
</li>
|
|
{pages.map((p) => (
|
|
<li key={p} className={`page-item ${p === page ? 'active' : ''}`}>
|
|
<button className="page-link" onClick={() => onPageChange(p)}>
|
|
{p}
|
|
</button>
|
|
</li>
|
|
))}
|
|
<li className={`page-item ${page >= lastPage ? 'disabled' : ''}`}>
|
|
<button className="page-link" onClick={() => onPageChange(page + 1)}>
|
|
Next
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
)}
|
|
</Card>
|
|
)
|
|
}
|