Changes to UI and other stuff
This commit is contained in:
@@ -1,14 +1,40 @@
|
||||
<script setup>
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { computed, ref } from "vue";
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationFirst,
|
||||
PaginationItem,
|
||||
PaginationLast,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/Components/ui/pagination";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-vue-next";
|
||||
import { toInteger } from "lodash";
|
||||
|
||||
const props = defineProps({
|
||||
links: { type: Array, default: () => [] },
|
||||
from: { type: Number, default: 0 },
|
||||
to: { type: Number, default: 0 },
|
||||
total: { type: Number, default: 0 },
|
||||
perPage: { type: Number, default: 15 },
|
||||
pageSizeOptions: { type: Array, default: () => [10, 15, 25, 50, 100] },
|
||||
currentPage: { type: Number, default: 0 },
|
||||
lastPage: { type: Number, default: 0 },
|
||||
perPageParam: { type: String, default: "per_page" }, // e.g., 'activities_per_page', 'contracts_per_page'
|
||||
pageParam: { type: String, default: "page" }, // e.g., 'activities_page', 'contracts_page'
|
||||
});
|
||||
|
||||
const num = props.links?.length || 0;
|
||||
@@ -53,44 +79,12 @@ const lastLink = computed(() => {
|
||||
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
|
||||
const total = props.lastPage;
|
||||
const current = props.currentPage;
|
||||
const maxVisible = 5;
|
||||
|
||||
if (total <= maxVisible) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
@@ -105,64 +99,55 @@ const visiblePages = computed(() => {
|
||||
let end = Math.min(total, start + maxVisible - 1);
|
||||
start = Math.max(1, Math.min(start, end - maxVisible + 1));
|
||||
|
||||
// Handle first page
|
||||
if (start > 1) {
|
||||
pages.push(1);
|
||||
if (start > 2) pages.push("...");
|
||||
}
|
||||
|
||||
// Add pages in window
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
// Handle last page
|
||||
if (end < total) {
|
||||
if (end < total - 1) pages.push("...");
|
||||
pages.push(total);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
// Navigate to a specific page using Laravel's pagination links
|
||||
function navigateToPage(pageNum) {
|
||||
if (!pageNum || pageNum < 1 || pageNum > props.lastPage) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(props.pageParam, String(pageNum));
|
||||
|
||||
router.get(
|
||||
url.pathname + url.search,
|
||||
{},
|
||||
{
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function goToPage() {
|
||||
const raw = String(gotoInput.value || "").trim();
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n) || n < 1 || n > lastPage.value) {
|
||||
if (!Number.isFinite(n) || n < 1 || n > props.lastPage) {
|
||||
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 = "";
|
||||
}
|
||||
navigateToPage(n);
|
||||
gotoInput.value = "";
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
@@ -170,39 +155,54 @@ function handleKeyPress(event) {
|
||||
goToPage();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePerPageChange(value) {
|
||||
const newPerPage = Number(value);
|
||||
if (!newPerPage) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(props.perPageParam, newPerPage);
|
||||
url.searchParams.set(props.pageParam, "1"); // Reset to first page
|
||||
|
||||
router.get(
|
||||
url.pathname + url.search,
|
||||
{},
|
||||
{
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
class="flex flex-wrap items-center justify-between gap-3 px-2 text-sm text-gray-700 sm:px-5"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<!-- Mobile: Simple prev/next -->
|
||||
<div class="flex flex-1 justify-between sm:hidden">
|
||||
<Link
|
||||
<button
|
||||
v-if="prevLink?.url"
|
||||
:href="prevLink.url"
|
||||
:preserve-scroll="false"
|
||||
@click="handleLinkClick"
|
||||
@click="navigateToPage(currentPage - 1)"
|
||||
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
|
||||
</Link>
|
||||
</button>
|
||||
<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
|
||||
<button
|
||||
v-if="nextLink?.url"
|
||||
:href="nextLink.url"
|
||||
:preserve-scroll="false"
|
||||
@click="handleLinkClick"
|
||||
@click="navigateToPage(currentPage + 1)"
|
||||
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
|
||||
</Link>
|
||||
</button>
|
||||
<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"
|
||||
@@ -213,159 +213,105 @@ function handleKeyPress(event) {
|
||||
|
||||
<!-- Desktop: Full pagination -->
|
||||
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||
<!-- 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>
|
||||
<!-- Page stats with modern badge style -->
|
||||
<div v-if="total > 0" class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">Prikazano</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-sm font-medium"
|
||||
>
|
||||
<span class="text-foreground">{{ from || 0 }}</span>
|
||||
<span class="text-muted-foreground">-</span>
|
||||
<span class="text-foreground">{{ to || 0 }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">od</span>
|
||||
<div
|
||||
class="inline-flex items-center rounded-md bg-primary/10 px-2.5 py-1 text-sm font-semibold text-primary"
|
||||
>
|
||||
{{ total || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="text-sm text-gray-700">Ni zadetkov</span>
|
||||
<div
|
||||
v-else
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5"
|
||||
>
|
||||
<span class="text-sm font-medium text-muted-foreground">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"
|
||||
>
|
||||
««
|
||||
</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>
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:total="total"
|
||||
:items-per-page="perPage"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
:default-page="currentPage"
|
||||
:page="currentPage"
|
||||
>
|
||||
<PaginationContent>
|
||||
<!-- First -->
|
||||
<PaginationFirst :disabled="currentPage <= 1" @click="navigateToPage(1)">
|
||||
<ChevronsLeft />
|
||||
</PaginationFirst>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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"
|
||||
<!-- Previous -->
|
||||
<PaginationPrevious
|
||||
:disabled="currentPage <= 1"
|
||||
@click="navigateToPage(currentPage - 1)"
|
||||
>
|
||||
{{ 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"
|
||||
<ChevronLeft />
|
||||
</PaginationPrevious>
|
||||
|
||||
<!-- Page numbers -->
|
||||
<template v-for="(item, index) in visiblePages" :key="index">
|
||||
<PaginationEllipsis v-if="item === '...'" />
|
||||
<PaginationItem
|
||||
v-else
|
||||
:value="item"
|
||||
:is-active="currentPage === item"
|
||||
@click="navigateToPage(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</PaginationItem>
|
||||
</template>
|
||||
|
||||
<!-- Next -->
|
||||
<PaginationNext
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="navigateToPage(currentPage + 1)"
|
||||
>
|
||||
{{ p }}
|
||||
</span>
|
||||
</template>
|
||||
<ChevronRight />
|
||||
</PaginationNext>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Last -->
|
||||
<PaginationLast
|
||||
:disabled="currentPage >= lastPage"
|
||||
@click="navigateToPage(lastPage)"
|
||||
>
|
||||
<ChevronsRight />
|
||||
</PaginationLast>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
|
||||
<!-- 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"
|
||||
<!-- Goto page input -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Go to page -->
|
||||
<div
|
||||
class="inline-flex items-center gap-2 rounded-md border border-input bg-background px-2 h-8"
|
||||
>
|
||||
»
|
||||
</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
|
||||
<input
|
||||
v-model="gotoInput"
|
||||
type="number"
|
||||
min="1"
|
||||
:max="lastPage"
|
||||
inputmode="numeric"
|
||||
class="w-16 text-sm"
|
||||
class="w-10 h-full text-sm text-center bg-transparent border-0 outline-none focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
:placeholder="String(currentPage)"
|
||||
aria-label="Pojdi na stran"
|
||||
@keyup.enter="goToPage"
|
||||
@blur="goToPage"
|
||||
/>
|
||||
<span class="text-sm text-gray-500">/ {{ lastPage }}</span>
|
||||
<Separator orientation="vertical" class="h-full" />
|
||||
<span class="text-sm text-muted-foreground">{{ lastPage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user