New Changes with Table Component
This commit is contained in:
@@ -90,7 +90,7 @@ const iconSize =
|
||||
const labelCls =
|
||||
"wr:block wr:text-sm wr:font-medium wr:mb-2 wr:dark:text-white";
|
||||
const helpCls = "wr:mt-2 wr:text-sm wr:text-gray-500 wr:dark:text-neutral-500";
|
||||
const errCls = "wr:text-sm wr:text-red-600 wr:mt-2";
|
||||
const errCls = "wr:text-sm wr:text-red-600 wr:mt-2 wr:capitalize";
|
||||
const groupCls = "wr:relative wr:flex wr:items-stretch";
|
||||
const addonBase =
|
||||
"wr:px-4 wr:inline-flex wr:items-center wr:min-w-fit wr:rounded-md wr:border wr:border-border wr:bg-gray-200 wr:text-sm wr:text-gray-500 wr:dark:bg-card wr:dark:border-border wr:dark:text-neutral-400";
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
---
|
||||
import type { TableProps } from "../types/table/types";
|
||||
|
||||
const {
|
||||
columns = [],
|
||||
data = [],
|
||||
selectable = false,
|
||||
search = false,
|
||||
striped = false,
|
||||
pageSize = 10,
|
||||
fetchData,
|
||||
columnsRendererKey,
|
||||
} = Astro.props as TableProps<any>;
|
||||
|
||||
const id = `table-${Math.random().toString(36).slice(2, 9)}`;
|
||||
---
|
||||
|
||||
<div id={id} class="wr:space-y-3">
|
||||
{
|
||||
search && (
|
||||
<div class="wr:flex wr:justify-between wr:items-center wr:gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="wr:w-full wr:rounded-md wr:border wr:border-gray-300 wr:dark:border-neutral-700 wr:bg-white wr:dark:bg-neutral-800 wr:px-3 wr:py-2 wr:text-sm wr:focus:ring-2 wr:focus:ring-primary"
|
||||
data-search
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- 🖥️ Desktop Table -->
|
||||
<div
|
||||
class="wr:overflow-x-auto wr:rounded-xl wr:border wr:dark:border-neutral-700 wr:hidden wr:md:block"
|
||||
>
|
||||
<table
|
||||
class={`wr:w-full wr:text-sm ${striped ? "wr:divide-y wr:divide-gray-200 wr:dark:divide-neutral-700" : ""}`}
|
||||
>
|
||||
<thead
|
||||
class="wr:bg-gray-100 wr:dark:bg-neutral-700 wr:text-gray-600 wr:dark:text-gray-300"
|
||||
>
|
||||
<tr>
|
||||
{
|
||||
selectable && (
|
||||
<th class="wr:p-3 wr:text-left wr:w-12">
|
||||
<input type="checkbox" data-select-all />
|
||||
</th>
|
||||
)
|
||||
}
|
||||
{
|
||||
columns.map((col) => (
|
||||
<th
|
||||
class={`wr:py-3 wr:px-4 ${col.align ? `wr:text-${col.align}` : "wr:text-left"} ${col.sortable ? "wr:cursor-pointer" : ""}`}
|
||||
style={col.width ? `width:${col.width}` : ""}
|
||||
data-sort-key={
|
||||
col.sortable ? col.key : undefined
|
||||
}
|
||||
>
|
||||
<div class="wr:flex wr:items-center wr:gap-1">
|
||||
<span>{col.label}</span>
|
||||
{col.sortable && (
|
||||
<iconify-icon
|
||||
icon="solar:sort-vertical-bold-duotone"
|
||||
class="wr:text-lg wr:text-gray-400 sort-indicator"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-body>
|
||||
<!-- {
|
||||
data.map((row, i) => (
|
||||
<tr
|
||||
class={`wr:transition wr:bg-white wr:dark:bg-neutral-900 wr:hover:bg-gray-100 wr:dark:hover:bg-neutral-700 ${striped ? "wr:border-b wr:dark:border-neutral-800" : ""}`}
|
||||
>
|
||||
{selectable && (
|
||||
<td class="wr:px-4 wr:py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-row-select
|
||||
data-index={i}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<td
|
||||
class={`wr:px-4 wr:py-3 ${col.align ? `wr:text-${col.align}` : "wr:text-left"} wr:whitespace-nowrap`}
|
||||
data-key={col.key}
|
||||
>
|
||||
{col.render ? (
|
||||
<Fragment
|
||||
set:html={col.render(
|
||||
row[col.key],
|
||||
row,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
String(row[col.key] ?? "")
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
} -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 📱 Mobile Cards -->
|
||||
<div class="wr:space-y-4 wr:md:hidden" data-mobile-body>
|
||||
<!-- {
|
||||
data.map((row, i) => (
|
||||
<div class="wr:rounded-lg wr:border wr:border-gray-200 wr:dark:border-neutral-700 wr:p-4 wr:space-y-2 wr:bg-white wr:dark:bg-neutral-800">
|
||||
{columns.map((col) => (
|
||||
<div class="wr:flex wr:justify-between wr:items-start wr:gap-4">
|
||||
<div class="wr:text-sm wr:font-medium wr:text-gray-600 wr:dark:text-gray-300">
|
||||
{col.label}
|
||||
</div>
|
||||
<div class="wr:text-sm wr:text-gray-800 wr:dark:text-gray-100 wr:text-right wr:max-w-[60%] wr:break-words">
|
||||
{col.render ? (
|
||||
<Fragment
|
||||
set:html={col.render(row[col.key], row)}
|
||||
/>
|
||||
) : (
|
||||
String(row[col.key] ?? "")
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
} -->
|
||||
</div>
|
||||
|
||||
<!-- 📄 Pagination -->
|
||||
<div
|
||||
class="wr:flex wr:justify-between wr:items-center wr:gap-4 wr:flex-wrap wr:mt-4"
|
||||
>
|
||||
<div class="wr:flex wr:items-center wr:gap-2 wr:text-sm">
|
||||
<span class="wr:text-gray-600 wr:dark:text-gray-400"
|
||||
>Rows per page:</span
|
||||
>
|
||||
<select
|
||||
class="wr:w-[75px] wr:rounded-md wr:border wr:border-gray-300 wr:dark:border-neutral-600 wr:bg-white wr:dark:bg-neutral-800 wr:px-2 wr:py-1 wr:text-sm wr:focus:ring-2 wr:focus:ring-primary"
|
||||
data-page-size
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="wr:flex wr:items-center wr:gap-3 wr:flex-wrap">
|
||||
<div
|
||||
class="wr:text-sm wr:text-gray-600 wr:dark:text-gray-400"
|
||||
data-page-info
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wr:flex wr:items-center wr:gap-1 wr:bg-gray-50 wr:dark:bg-neutral-800 wr:rounded-full wr:px-2 wr:py-1 wr:shadow-sm"
|
||||
data-pagination
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
type="module"
|
||||
is:inline
|
||||
define:vars={{ id, pageSize, fetchData, columns, data, columnsRendererKey }}
|
||||
>
|
||||
const start = () => {
|
||||
const table = document.getElementById(id);
|
||||
|
||||
const searchInput = table.querySelector("[data-search]");
|
||||
const sortHeaders = table.querySelectorAll("[data-sort-key]");
|
||||
const tbody = table.querySelector("[data-body]");
|
||||
const mobileBody = table.querySelector("[data-mobile-body]");
|
||||
const pageInfo = table.querySelector("[data-page-info]");
|
||||
const paginationContainer = table.querySelector("[data-pagination]");
|
||||
const pageSizeSelector = table.querySelector("[data-page-size]");
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = parseInt(pageSizeSelector.value);
|
||||
let sortKey = null;
|
||||
let sortOrder = "asc";
|
||||
let totalRows = data.length;
|
||||
let currentData = [...data];
|
||||
|
||||
const arrowLeftSVG = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="wr:w-5 wr:h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>`;
|
||||
|
||||
const arrowRightSVG = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="wr:w-5 wr:h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>`;
|
||||
|
||||
const fetchDataFn = fetchData ? window[fetchData] : null;
|
||||
|
||||
/** 🧱 Render rows */
|
||||
function renderRows(dataset) {
|
||||
tbody.innerHTML = "";
|
||||
mobileBody.innerHTML = "";
|
||||
|
||||
dataset.forEach((row) => {
|
||||
// 🖥️ Desktop
|
||||
let tr = `<tr class="wr:transition wr:bg-white wr:dark:bg-neutral-900 wr:hover:bg-gray-100 wr:dark:hover:bg-neutral-700 wr:border-b wr:dark:border-neutral-800">`;
|
||||
columns.forEach((col) => {
|
||||
let value = row[col.key] ?? "";
|
||||
|
||||
const renderers = columnsRendererKey
|
||||
? window[columnsRendererKey]
|
||||
: {};
|
||||
columns.forEach((col) => {
|
||||
if (renderers[col.key]) col.render = renderers[col.key];
|
||||
});
|
||||
|
||||
if (col.render) {
|
||||
value = col.render(value, row);
|
||||
}
|
||||
tr += `<td class="wr:px-4 wr:py-3">${value}</td>`;
|
||||
});
|
||||
tr += `</tr>`;
|
||||
tbody.insertAdjacentHTML("beforeend", tr);
|
||||
|
||||
// 📱 Mobile
|
||||
let card = `<div class="wr:rounded-lg wr:border wr:border-gray-200 wr:dark:border-neutral-700 wr:p-4 wr:space-y-2 wr:bg-white wr:dark:bg-neutral-800">`;
|
||||
columns.forEach((col) => {
|
||||
let value = row[col.key] ?? "";
|
||||
|
||||
const renderers = columnsRendererKey
|
||||
? window[columnsRendererKey]
|
||||
: {};
|
||||
columns.forEach((col) => {
|
||||
if (renderers[col.key]) col.render = renderers[col.key];
|
||||
});
|
||||
|
||||
if (col.render) {
|
||||
value = col.render(value, row);
|
||||
}
|
||||
card += `
|
||||
<div class="wr:flex wr:justify-between wr:items-start wr:gap-4">
|
||||
<div class="wr:text-sm wr:font-medium wr:text-gray-600 wr:dark:text-gray-300">${col.label}</div>
|
||||
<div class="wr:text-sm wr:text-gray-800 wr:dark:text-gray-100 wr:text-right wr:max-w-[60%] wr:break-words">${value}</div>
|
||||
</div>`;
|
||||
});
|
||||
card += `</div>`;
|
||||
mobileBody.insertAdjacentHTML("beforeend", card);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
let paginated = [];
|
||||
|
||||
if (fetchDataFn) {
|
||||
const result = await fetchDataFn({
|
||||
page: currentPage,
|
||||
pageSize: rowsPerPage,
|
||||
sortKey,
|
||||
sortOrder,
|
||||
search: searchInput?.value || "",
|
||||
});
|
||||
currentData = result.data;
|
||||
totalRows = result.total;
|
||||
|
||||
paginated = currentData;
|
||||
} else {
|
||||
currentData = [...data];
|
||||
totalRows = currentData.length;
|
||||
|
||||
const start = (currentPage - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
paginated = currentData.slice(start, end);
|
||||
}
|
||||
|
||||
// sort locally if needed
|
||||
if (sortKey !== null) {
|
||||
const colKey = columns[sortKey]?.key;
|
||||
if (colKey) {
|
||||
currentData.sort((a, b) => {
|
||||
const aVal = String(a[colKey] ?? "");
|
||||
const bVal = String(b[colKey] ?? "");
|
||||
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
|
||||
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// paginate
|
||||
|
||||
renderRows(paginated);
|
||||
renderPagination(totalRows);
|
||||
}
|
||||
|
||||
/** 📄 Pagination */
|
||||
function renderPagination(totalRows) {
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
paginationContainer.innerHTML = "";
|
||||
|
||||
const createButton = (
|
||||
label,
|
||||
page,
|
||||
disabled = false,
|
||||
isActive = false,
|
||||
icon = null,
|
||||
) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `
|
||||
wr:flex wr:items-center wr:justify-center wr:w-8 wr:h-8
|
||||
wr:rounded-full wr:text-sm wr:transition
|
||||
${
|
||||
isActive
|
||||
? "wr:bg-primary wr:text-white wr:shadow-md"
|
||||
: "wr:text-gray-600 wr:dark:text-gray-300 wr:hover:bg-gray-200 wr:dark:hover:bg-neutral-700"
|
||||
}
|
||||
${disabled ? "wr:opacity-50 wr:cursor-not-allowed" : ""}
|
||||
`;
|
||||
if (icon === "left") btn.innerHTML = arrowLeftSVG;
|
||||
else if (icon === "right") btn.innerHTML = arrowRightSVG;
|
||||
else btn.textContent = label;
|
||||
|
||||
if (!disabled && !isActive) {
|
||||
btn.addEventListener("click", () => {
|
||||
currentPage = page;
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
paginationContainer.appendChild(btn);
|
||||
};
|
||||
|
||||
createButton("", currentPage - 1, currentPage === 1, false, "left");
|
||||
|
||||
const maxVisible = 5;
|
||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
||||
let end = Math.min(totalPages, start + maxVisible - 1);
|
||||
if (end - start < maxVisible - 1)
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
|
||||
for (let i = start; i <= end; i++)
|
||||
createButton(i, i, false, i === currentPage);
|
||||
|
||||
createButton(
|
||||
"",
|
||||
currentPage + 1,
|
||||
currentPage === totalPages,
|
||||
false,
|
||||
"right",
|
||||
);
|
||||
|
||||
pageInfo.textContent = `Page ${currentPage} of ${totalPages} • ${totalRows} total`;
|
||||
}
|
||||
|
||||
// 🕵️ Search
|
||||
searchInput?.addEventListener("input", () => {
|
||||
currentPage = 1;
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 🧭 Sorting
|
||||
sortHeaders.forEach((th, index) => {
|
||||
th.addEventListener("click", () => {
|
||||
if (sortKey === index)
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
else {
|
||||
sortKey = index;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
|
||||
sortHeaders.forEach((h) =>
|
||||
h
|
||||
.querySelector(".sort-indicator")
|
||||
?.setAttribute(
|
||||
"icon",
|
||||
"solar:sort-vertical-bold-duotone",
|
||||
),
|
||||
);
|
||||
th.querySelector(".sort-indicator")?.setAttribute(
|
||||
"icon",
|
||||
sortOrder === "asc"
|
||||
? "solar:sort-up-bold-duotone"
|
||||
: "solar:sort-down-bold-duotone",
|
||||
);
|
||||
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
|
||||
// 📊 Page size
|
||||
pageSizeSelector?.addEventListener("change", (e) => {
|
||||
rowsPerPage = parseInt(e.target.value);
|
||||
currentPage = 1;
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 🚀 Initial
|
||||
loadData();
|
||||
};
|
||||
|
||||
document.addEventListener("astro:page-load", start);
|
||||
document.addEventListener("DOMContentLoaded", start);
|
||||
</script>
|
||||
Vendored
+2
@@ -6,6 +6,8 @@ declare global {
|
||||
schema: any;
|
||||
submit: any;
|
||||
upload: any;
|
||||
fetchData: any;
|
||||
columnsRenderer: any
|
||||
myModal_open: any;
|
||||
myModal_close: any;
|
||||
hls: typeof Hls,
|
||||
|
||||
+3
-1
@@ -17,5 +17,7 @@ export { default as Panel } from "./components/Panel.astro";
|
||||
export { default as PasswordField } from "./components/PasswordField.astro";
|
||||
export { default as ThemeSwitcher } from "./components/ThemeSwitcher.astro";
|
||||
export { default as TimePicker } from "./components/TimePicker.astro";
|
||||
export { default as Table } from "./components/Table.astro";
|
||||
export { default as Tooltip } from "./components/Tooltip.astro";
|
||||
export { cn } from "./utils/cn.js";
|
||||
export { cn } from "./utils/cn.js";
|
||||
export type { Column, TableProps } from "./types/table/types.js";
|
||||
@@ -83,6 +83,9 @@ import Base from "./Base.astro";
|
||||
<li class="list-item">
|
||||
<a href="/webrtc-player">WebRTC Player</a>
|
||||
</li>
|
||||
<li class="list-item">
|
||||
<a href="/table">Table</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
---
|
||||
import ComponentLayout from "../layouts/ComponentLayout.astro";
|
||||
import Table from "../components/Table.astro";
|
||||
|
||||
export const columns = [
|
||||
{ key: "id", label: "ID", sortable: true, searchable: true },
|
||||
{ key: "name", label: "Name", searchable: true, sortable: true },
|
||||
{ key: "email", label: "Email", searchable: true, sortable: true },
|
||||
{ key: "role", label: "Role", sortable: true },
|
||||
{ key: "status", label: "Status", sortable: true },
|
||||
{ key: "created_at", label: "Created At", sortable: true },
|
||||
{ key: "actions", label: "Actions" },
|
||||
];
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Ajay Ghanwat",
|
||||
email: "ajay.gh@example.com",
|
||||
role: "Admin",
|
||||
status: "Active",
|
||||
created_at: "2025-10-10 09:32",
|
||||
avatar: "https://i.pravatar.cc/40?img=1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Rahul Sharma",
|
||||
email: "rahul.sharma@example.com",
|
||||
role: "Member",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-15 11:42",
|
||||
avatar: "https://i.pravatar.cc/40?img=2",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Sophia Patel",
|
||||
email: "sophia.patel@example.com",
|
||||
role: "Manager",
|
||||
status: "Active",
|
||||
created_at: "2025-10-11 15:20",
|
||||
avatar: "https://i.pravatar.cc/40?img=3",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Liam Brown",
|
||||
email: "liam.brown@example.com",
|
||||
role: "Member",
|
||||
status: "Suspended",
|
||||
created_at: "2025-10-12 08:45",
|
||||
avatar: "https://i.pravatar.cc/40?img=4",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Olivia Smith",
|
||||
email: "olivia.smith@example.com",
|
||||
role: "Admin",
|
||||
status: "Active",
|
||||
created_at: "2025-10-09 18:12",
|
||||
avatar: "https://i.pravatar.cc/40?img=5",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Noah Wilson",
|
||||
email: "noah.wilson@example.com",
|
||||
role: "Member",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-13 10:18",
|
||||
avatar: "https://i.pravatar.cc/40?img=6",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Emma Johnson",
|
||||
email: "emma.johnson@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-14 13:55",
|
||||
avatar: "https://i.pravatar.cc/40?img=7",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "William Lee",
|
||||
email: "william.lee@example.com",
|
||||
role: "Manager",
|
||||
status: "Inactive",
|
||||
created_at: "2025-10-12 17:44",
|
||||
avatar: "https://i.pravatar.cc/40?img=8",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Isabella Taylor",
|
||||
email: "isabella.taylor@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-10 07:31",
|
||||
avatar: "https://i.pravatar.cc/40?img=9",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Ethan Martinez",
|
||||
email: "ethan.martinez@example.com",
|
||||
role: "Admin",
|
||||
status: "Suspended",
|
||||
created_at: "2025-10-08 19:22",
|
||||
avatar: "https://i.pravatar.cc/40?img=10",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Mia Thompson",
|
||||
email: "mia.thompson@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-09 09:45",
|
||||
avatar: "https://i.pravatar.cc/40?img=11",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Alexander Davis",
|
||||
email: "alex.davis@example.com",
|
||||
role: "Manager",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-15 14:10",
|
||||
avatar: "https://i.pravatar.cc/40?img=12",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Charlotte Moore",
|
||||
email: "charlotte.moore@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-14 10:01",
|
||||
avatar: "https://i.pravatar.cc/40?img=13",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Benjamin Anderson",
|
||||
email: "benjamin.anderson@example.com",
|
||||
role: "Member",
|
||||
status: "Suspended",
|
||||
created_at: "2025-10-08 11:32",
|
||||
avatar: "https://i.pravatar.cc/40?img=14",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Amelia Harris",
|
||||
email: "amelia.harris@example.com",
|
||||
role: "Manager",
|
||||
status: "Active",
|
||||
created_at: "2025-10-13 08:50",
|
||||
avatar: "https://i.pravatar.cc/40?img=15",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Lucas White",
|
||||
email: "lucas.white@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-10 17:10",
|
||||
avatar: "https://i.pravatar.cc/40?img=16",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Harper Lewis",
|
||||
email: "harper.lewis@example.com",
|
||||
role: "Member",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-14 09:00",
|
||||
avatar: "https://i.pravatar.cc/40?img=17",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Evelyn Hall",
|
||||
email: "evelyn.hall@example.com",
|
||||
role: "Manager",
|
||||
status: "Active",
|
||||
created_at: "2025-10-11 16:42",
|
||||
avatar: "https://i.pravatar.cc/40?img=18",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Henry Allen",
|
||||
email: "henry.allen@example.com",
|
||||
role: "Member",
|
||||
status: "Inactive",
|
||||
created_at: "2025-10-09 13:25",
|
||||
avatar: "https://i.pravatar.cc/40?img=19",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Ava Scott",
|
||||
email: "ava.scott@example.com",
|
||||
role: "Admin",
|
||||
status: "Active",
|
||||
created_at: "2025-10-15 15:33",
|
||||
avatar: "https://i.pravatar.cc/40?img=20",
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: "Michael Young",
|
||||
email: "michael.young@example.com",
|
||||
role: "Member",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-13 10:20",
|
||||
avatar: "https://i.pravatar.cc/40?img=21",
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: "Ella King",
|
||||
email: "ella.king@example.com",
|
||||
role: "Manager",
|
||||
status: "Active",
|
||||
created_at: "2025-10-14 08:45",
|
||||
avatar: "https://i.pravatar.cc/40?img=22",
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: "Daniel Clark",
|
||||
email: "daniel.clark@example.com",
|
||||
role: "Member",
|
||||
status: "Active",
|
||||
created_at: "2025-10-12 12:05",
|
||||
avatar: "https://i.pravatar.cc/40?img=23",
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: "Luna Wright",
|
||||
email: "luna.wright@example.com",
|
||||
role: "Member",
|
||||
status: "Pending",
|
||||
created_at: "2025-10-15 11:18",
|
||||
avatar: "https://i.pravatar.cc/40?img=24",
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
name: "Jack Turner",
|
||||
email: "jack.turner@example.com",
|
||||
role: "Admin",
|
||||
status: "Active",
|
||||
created_at: "2025-10-13 09:50",
|
||||
avatar: "https://i.pravatar.cc/40?img=25",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<ComponentLayout>
|
||||
<div class="wr:p-4">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
columnsRendererKey="columnsRenderer"
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
fetchData="fetchData"
|
||||
columnsRendererKey="columnsRenderer"
|
||||
/>
|
||||
</div>
|
||||
</ComponentLayout>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const buttons = document.querySelectorAll(
|
||||
"button[id^='view-'], button[id^='edit-'], button[id^='delete-']",
|
||||
);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
const id = button.getAttribute("data-id");
|
||||
|
||||
if (button.id.startsWith("view-")) {
|
||||
button.addEventListener("click", () => {
|
||||
console.log(`👁️ View user ${id}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (button.id.startsWith("edit-")) {
|
||||
button.addEventListener("click", () => {
|
||||
console.log(`✏️ Edit user ${id}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (button.id.startsWith("delete-")) {
|
||||
button.addEventListener("click", () => {
|
||||
if (
|
||||
confirm(`Are you sure you want to delete user ${id}?`)
|
||||
) {
|
||||
console.log(`🗑️ Deleted user ${id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.columnsRenderer = {
|
||||
name: (v: any, row: any) => `
|
||||
<div class="wr:flex wr:items-center wr:gap-2">
|
||||
<img src="${row.avatar}" alt="${v}" class="wr:w-8 wr:h-8 wr:rounded-full" />
|
||||
<span class="wr:font-medium">${v}</span>
|
||||
</div>
|
||||
`,
|
||||
|
||||
role: (v: string) => `
|
||||
<span class="wr:rounded-full wr:px-2 wr:py-1 wr:text-xs ${
|
||||
v === "Admin"
|
||||
? "wr:bg-indigo-100 wr:text-indigo-700"
|
||||
: v === "Owner"
|
||||
? "wr:bg-yellow-100 wr:text-yellow-700"
|
||||
: "wr:bg-gray-100 wr:text-gray-600"
|
||||
}">${v}</span>
|
||||
`,
|
||||
|
||||
status: (v: string) => `
|
||||
<span class="wr:flex wr:items-center wr:gap-1 wr:font-medium">
|
||||
<span class="wr:w-2 wr:h-2 wr:rounded-full ${
|
||||
v === "Active"
|
||||
? "wr:bg-green-500"
|
||||
: v === "Inactive"
|
||||
? "wr:bg-red-500"
|
||||
: "wr:bg-yellow-500"
|
||||
}"></span>
|
||||
${v}
|
||||
</span>
|
||||
`,
|
||||
|
||||
actions: (v: any, row: any) => `
|
||||
<div class="wr:flex wr:gap-2 wr:justify-end">
|
||||
<button id="view-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-blue-500 wr:text-white wr:rounded wr:hover:bg-blue-600">View</button>
|
||||
<button id="edit-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-amber-500 wr:text-white wr:rounded wr:hover:bg-amber-600">Edit</button>
|
||||
<button id="delete-${row.id}" data-id="${row.id}" class="wr:px-2 wr:py-1 wr:bg-red-500 wr:text-white wr:rounded wr:hover:bg-red-600">Delete</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
window.fetchData = ({
|
||||
page,
|
||||
pageSize,
|
||||
sortKey,
|
||||
sortOrder,
|
||||
search,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey?: string | null;
|
||||
sortOrder?: "asc" | "desc";
|
||||
search?: string;
|
||||
}) => {
|
||||
// 🧪 Generate 100 fake users
|
||||
const demoData = Array.from({ length: 100 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
name: `User ${i + 1}`,
|
||||
avatar: `https://i.pravatar.cc/40?img=${i + 1}`,
|
||||
email: `user${i + 1}@example.com`,
|
||||
role: ["Admin", "Manager", "Member"][i % 3],
|
||||
status: ["Active", "Pending", "Suspended"][i % 3],
|
||||
created_at: new Date(
|
||||
2025,
|
||||
0,
|
||||
1 + (i % 28),
|
||||
12,
|
||||
i % 60,
|
||||
).toLocaleString(),
|
||||
}));
|
||||
|
||||
// 🔍 Searching
|
||||
let filtered = demoData;
|
||||
if (search && search.trim() !== "") {
|
||||
const lower = search.toLowerCase();
|
||||
filtered = demoData.filter((row) =>
|
||||
Object.values(row).some((val) =>
|
||||
String(val).toLowerCase().includes(lower),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🧭 Sorting
|
||||
if (sortKey) {
|
||||
filtered.sort((a, b) => {
|
||||
const aVal = String(a[sortKey as keyof typeof a]);
|
||||
const bVal = String(b[sortKey as keyof typeof b]);
|
||||
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
|
||||
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 📄 Pagination
|
||||
const total = filtered.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const pageData = filtered.slice(start, end);
|
||||
|
||||
// ✅ Return object data, not HTML
|
||||
return {
|
||||
data: pageData,
|
||||
total,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface Column<T> {
|
||||
key: keyof T | string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
searchable?: boolean;
|
||||
render?: (value: any, row: T) => string;
|
||||
width?: string;
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export interface TableProps<T> {
|
||||
columns: Column<T>[];
|
||||
data?: T[];
|
||||
fetchData?: string;
|
||||
selectable?: boolean;
|
||||
search?: boolean;
|
||||
striped?: boolean;
|
||||
pageSize?: number
|
||||
columnsRendererKey?: string
|
||||
}
|
||||
Reference in New Issue
Block a user