368 lines
13 KiB
Plaintext
368 lines
13 KiB
Plaintext
---
|
|
import type { TableProps } from "../types/table/types";
|
|
import { cn } from "../utils/cn";
|
|
|
|
const {
|
|
columns = [],
|
|
data = [],
|
|
selectable = false,
|
|
search = false,
|
|
striped = false,
|
|
pagignations = true,
|
|
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:border-gray-300 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> </tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 📱 Mobile Cards -->
|
|
<div class="wr:space-y-4 wr:md:hidden" data-mobile-body></div>
|
|
|
|
<!-- 📄 Pagination -->
|
|
<div
|
|
class={cn(
|
|
"wr:flex wr:justify-between wr:items-center wr:gap-4 wr:flex-wrap wr:mt-4",
|
|
!pagignations && "wr:!hidden",
|
|
)}
|
|
>
|
|
<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,
|
|
pagignations,
|
|
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 = pagignations
|
|
? parseInt(pageSizeSelector.value)
|
|
: 9999;
|
|
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, i) => {
|
|
// 🖥️ 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 ${dataset.length - 1 != i && "wr:border-b"} wr:border-gray-300 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-300 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>
|