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>
|
||||
Reference in New Issue
Block a user