Dev branch

This commit is contained in:
Simon Pocrnjič
2025-11-02 12:31:01 +01:00
parent 5f879c9436
commit 63e0958b66
241 changed files with 17686 additions and 7327 deletions
+341 -168
View File
@@ -1,6 +1,8 @@
<script setup>
import { Link } from "@inertiajs/vue3";
import { computed } from "vue";
import { Link, router } from "@inertiajs/vue3";
import { computed, ref } from "vue";
import { Input } from '@/Components/ui/input';
import { Button } from '@/Components/ui/button';
const props = defineProps({
links: { type: Array, default: () => [] },
@@ -11,190 +13,361 @@ const props = defineProps({
const num = props.links?.length || 0;
const prevLink = computed(() => (num > 0 ? props.links[0] : null));
const nextLink = computed(() => (num > 1 ? props.links[num - 1] : null));
const numericLinks = computed(() => {
if (num < 3) return [];
return props.links
.slice(1, num - 1)
.map((l) => ({
...l,
page: Number.parseInt(String(l.label).replace(/[^0-9]/g, ""), 10),
}))
.filter((l) => !Number.isNaN(l.page));
});
const currentPage = computed(() => numericLinks.value.find((l) => l.active)?.page || 1);
const lastPage = computed(() =>
numericLinks.value.length ? Math.max(...numericLinks.value.map((l) => l.page)) : 1
);
const linkByPage = computed(() => {
const m = new Map();
for (const l of numericLinks.value) m.set(l.page, l);
return m;
});
const windowItems = computed(() => {
const items = [];
const cur = currentPage.value;
const last = lastPage.value;
const show = new Set([1, last, cur - 1, cur, cur + 1]);
if (cur <= 3) {
show.add(2);
show.add(3);
const prevLink = computed(() => {
if (num > 0 && props.links && Array.isArray(props.links) && props.links[0]) {
return props.links[0];
}
if (cur >= last - 2) {
show.add(last - 1);
show.add(last - 2);
return null;
});
const nextLink = computed(() => {
if (num > 1 && props.links && Array.isArray(props.links) && props.links[num - 1]) {
return props.links[num - 1];
}
// Prev
items.push({ kind: "prev", link: prevLink.value });
// Pages with ellipses
let inGap = false;
for (let p = 1; p <= last; p++) {
if (show.has(p)) {
items.push({
kind: "page",
link: linkByPage.value.get(p) || {
url: null,
label: String(p),
active: p === cur,
},
});
inGap = false;
} else if (!inGap) {
items.push({ kind: "ellipsis" });
inGap = true;
return null;
});
const firstLink = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return null;
// Find the first numeric link (page 1)
for (let i = 1; i < num - 1; i++) {
const link = props.links[i];
if (!link) continue;
const page = Number.parseInt(String(link?.label || "").replace(/[^0-9]/g, ""), 10);
if (page === 1) return link;
}
return null;
});
const lastLink = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return null;
// Find the last numeric link
let maxPage = 0;
let maxLink = null;
for (let i = 1; i < num - 1; i++) {
const link = props.links[i];
if (!link) continue;
const page = Number.parseInt(String(link?.label || "").replace(/[^0-9]/g, ""), 10);
if (!Number.isNaN(page) && page > maxPage) {
maxPage = page;
maxLink = link;
}
}
// Next
items.push({ kind: "next", link: nextLink.value });
return items;
return maxLink;
});
const numericLinks = computed(() => {
if (num < 3 || !props.links || !Array.isArray(props.links)) return [];
return props.links
.slice(1, num - 1)
.filter((l) => l != null)
.map((l) => ({
...l,
page: Number.parseInt(String(l?.label || "").replace(/[^0-9]/g, ""), 10),
}))
.filter((l) => !Number.isNaN(l.page) && l.page != null);
});
const currentPage = computed(() => {
const active = numericLinks.value.find((l) => l?.active);
return active?.page || 1;
});
const lastPage = computed(() => {
if (!numericLinks.value.length) return 1;
const pages = numericLinks.value.map((l) => l?.page).filter(p => p != null);
return pages.length ? Math.max(...pages) : 1;
});
const linkByPage = computed(() => {
const m = new Map();
for (const l of numericLinks.value) {
if (l?.page != null) {
m.set(l.page, l);
}
}
return m;
});
// Generate visible page numbers with ellipsis (similar to DataTableClient)
const visiblePages = computed(() => {
const pages = [];
const total = lastPage.value;
const current = currentPage.value;
const maxVisible = 5; // Match DataTableClient default
if (total <= maxVisible) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
return pages;
}
// Calculate window around current page
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + maxVisible - 1);
start = Math.max(1, Math.min(start, end - maxVisible + 1));
// Add pages in window
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
});
const gotoInput = ref("");
// Handle scroll on navigation
function handleLinkClick(event) {
// Prevent default scroll behavior
event.preventDefault();
const href = event.currentTarget.getAttribute('href');
if (href) {
router.visit(href, {
preserveScroll: false,
onSuccess: () => {
// Scroll to top of table after navigation completes
setTimeout(() => {
const tableElement = document.querySelector('[data-table-container]');
if (tableElement) {
tableElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}, 100);
},
});
}
}
function goToPage() {
const raw = String(gotoInput.value || "").trim();
const n = Number(raw);
if (!Number.isFinite(n) || n < 1 || n > lastPage.value) {
gotoInput.value = "";
return;
}
const targetLink = linkByPage.value.get(Math.floor(n));
if (targetLink?.url) {
router.visit(targetLink.url, {
preserveScroll: false,
onSuccess: () => {
// Scroll to top of table when page changes
const tableElement = document.querySelector('[data-table-container]');
if (tableElement) {
tableElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
},
});
} else {
// If link not found, try to construct URL manually
gotoInput.value = "";
}
}
function handleKeyPress(event) {
if (event.key === "Enter") {
goToPage();
}
}
</script>
<template>
<div class="flex items-center justify-between bg-white px-4 py-3 sm:px-6">
<!-- Mobile: Prev / Next -->
<nav
class="flex flex-wrap items-center justify-between gap-3 border-t border-gray-200 bg-white px-4 py-3 text-sm text-gray-700 sm:px-6"
aria-label="Pagination"
>
<!-- Mobile: Simple prev/next -->
<div class="flex flex-1 justify-between sm:hidden">
<component
:is="links?.[0]?.url ? Link : 'span'"
:href="links?.[0]?.url"
:aria-disabled="!links?.[0]?.url"
:tabindex="!links?.[0]?.url ? -1 : 0"
class="relative inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium"
:class="
links?.[0]?.url
? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
: 'border-gray-200 bg-gray-100 text-gray-400'
"
<Link
v-if="prevLink?.url"
:href="prevLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
>
Prejšnja
</component>
<component
:is="links?.[num - 1]?.url ? Link : 'span'"
:href="links?.[num - 1]?.url"
:aria-disabled="!links?.[num - 1]?.url"
:tabindex="!links?.[num - 1]?.url ? -1 : 0"
class="relative ml-3 inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium"
:class="
links?.[num - 1]?.url
? 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
: 'border-gray-200 bg-gray-100 text-gray-400'
"
</Link>
<span
v-else
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-400 cursor-not-allowed opacity-50"
>
Prejšnja
</span>
<Link
v-if="nextLink?.url"
:href="nextLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
>
Naslednja
</component>
</Link>
<span
v-else
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-400 cursor-not-allowed opacity-50"
>
Naslednja
</span>
</div>
<!-- Desktop: Full pagination -->
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing
<span class="font-medium">{{ from }}</span>
to
<span class="font-medium">{{ to }}</span>
of
<span class="font-medium">{{ total }}</span>
results
</p>
<!-- Page stats -->
<div v-if="total > 0">
<span class="text-sm text-gray-700">
Prikazano: <span class="font-medium">{{ from || 0 }}</span><span class="font-medium">{{ to || 0 }}</span> od
<span class="font-medium">{{ total || 0 }}</span>
</span>
</div>
<div>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
<div v-else>
<span class="text-sm text-gray-700">Ni zadetkov</span>
</div>
<!-- Pagination controls -->
<div class="flex items-center gap-1">
<!-- First -->
<Link
v-if="firstLink?.url && currentPage > 1"
:href="firstLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Prva stran"
>
<template v-for="(item, idx) in windowItems" :key="idx">
<!-- Prev / Next -->
<component
v-if="item.kind === 'prev' || item.kind === 'next'"
:is="item.link?.url ? Link : 'span'"
:href="item.link?.url"
class="relative inline-flex items-center px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0"
:class="{
'rounded-l-md': item.kind === 'prev',
'rounded-r-md': item.kind === 'next',
'text-gray-900 hover:bg-gray-50': item.link?.url,
'text-gray-400 bg-gray-100': !item.link?.url,
}"
>
<span class="sr-only">{{
item.kind === "prev" ? "Prejšnja" : "Naslednja"
}}</span>
<svg
v-if="item.kind === 'prev'"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
<svg
v-else
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</component>
««
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Prva stran"
>
««
</span>
<!-- Ellipsis -->
<span
v-else-if="item.kind === 'ellipsis'"
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-500 ring-1 ring-inset ring-gray-200 select-none"
></span
>
<!-- Prev -->
<Link
v-if="prevLink?.url"
:href="prevLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Prejšnja stran"
>
«
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Prejšnja stran"
>
«
</span>
<!-- Page number -->
<component
v-else-if="item.kind === 'page'"
:is="item.link?.url ? Link : 'span'"
:href="item.link?.url"
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold focus:outline-offset-0"
:class="{
'text-gray-700 ring-1 ring-inset ring-gray-300': !item.link?.url,
'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20':
item.link?.url && !item.link?.active,
'z-10 bg-blue-600 text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600':
item.link?.active,
}"
>
{{ item.link?.label || "" }}
</component>
</template>
</nav>
<!-- Leading ellipsis / first page when window doesn't include 1 -->
<Link
v-if="visiblePages[0] > 1"
:href="firstLink?.url || '#'"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-900 hover:bg-gray-50 transition-colors"
>
1
</Link>
<span v-if="visiblePages[0] > 2" class="px-1 text-gray-700">…</span>
<!-- Page numbers -->
<template v-for="p in visiblePages" :key="p">
<Link
v-if="linkByPage.get(p)?.url"
:href="linkByPage.get(p).url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
:class="
p === currentPage
? 'border-primary-600 bg-primary-600 text-white'
: 'border-gray-300 bg-white text-gray-900 hover:bg-gray-50'
"
:aria-current="p === currentPage ? 'page' : undefined"
>
{{ p }}
</Link>
<span
v-else
class="px-3 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
>
{{ p }}
</span>
</template>
<!-- Trailing ellipsis / last page when window doesn't include last -->
<span v-if="visiblePages[visiblePages.length - 1] < lastPage - 1" class="px-1 text-gray-700"></span>
<Link
v-if="visiblePages[visiblePages.length - 1] < lastPage && lastLink?.url"
:href="lastLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-900 hover:bg-gray-50 transition-colors"
>
{{ lastPage }}
</Link>
<!-- Next -->
<Link
v-if="nextLink?.url"
:href="nextLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Naslednja stran"
>
»
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Naslednja stran"
>
»
</span>
<!-- Last -->
<Link
v-if="lastLink?.url && currentPage < lastPage"
:href="lastLink.url"
:preserve-scroll="false"
@click="handleLinkClick"
class="px-2 py-1 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1"
aria-label="Zadnja stran"
>
»»
</Link>
<span
v-else
class="px-2 py-1 rounded border border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed opacity-50"
aria-label="Zadnja stran"
>
»»
</span>
<!-- Goto page input -->
<div class="ms-2 flex items-center gap-1">
<Input
v-model="gotoInput"
type="number"
min="1"
:max="lastPage"
inputmode="numeric"
class="w-16 text-sm"
:placeholder="String(currentPage)"
aria-label="Pojdi na stran"
@keyup.enter="goToPage"
@blur="goToPage"
/>
<span class="text-sm text-gray-500">/ {{ lastPage }}</span>
</div>
</div>
</div>
</div>
</nav>
</template>