320 lines
9.4 KiB
Vue
320 lines
9.4 KiB
Vue
<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 {
|
|
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;
|
|
|
|
const prevLink = computed(() => {
|
|
if (num > 0 && props.links && Array.isArray(props.links) && props.links[0]) {
|
|
return props.links[0];
|
|
}
|
|
return null;
|
|
});
|
|
const nextLink = computed(() => {
|
|
if (num > 1 && props.links && Array.isArray(props.links) && props.links[num - 1]) {
|
|
return props.links[num - 1];
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
return maxLink;
|
|
});
|
|
|
|
// Generate visible page numbers with ellipsis (similar to DataTableClient)
|
|
const visiblePages = computed(() => {
|
|
const pages = [];
|
|
const total = props.lastPage;
|
|
const current = props.currentPage;
|
|
const maxVisible = 5;
|
|
|
|
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));
|
|
|
|
// 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("");
|
|
|
|
// 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 > props.lastPage) {
|
|
gotoInput.value = "";
|
|
return;
|
|
}
|
|
navigateToPage(n);
|
|
gotoInput.value = "";
|
|
}
|
|
|
|
function handleKeyPress(event) {
|
|
if (event.key === "Enter") {
|
|
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 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">
|
|
<button
|
|
v-if="prevLink?.url"
|
|
@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
|
|
</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>
|
|
<button
|
|
v-if="nextLink?.url"
|
|
@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
|
|
</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"
|
|
>
|
|
Naslednja
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Desktop: Full pagination -->
|
|
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
|
<!-- 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
|
|
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 -->
|
|
<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>
|
|
|
|
<!-- Previous -->
|
|
<PaginationPrevious
|
|
:disabled="currentPage <= 1"
|
|
@click="navigateToPage(currentPage - 1)"
|
|
>
|
|
<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)"
|
|
>
|
|
<ChevronRight />
|
|
</PaginationNext>
|
|
|
|
<!-- Last -->
|
|
<PaginationLast
|
|
:disabled="currentPage >= lastPage"
|
|
@click="navigateToPage(lastPage)"
|
|
>
|
|
<ChevronsRight />
|
|
</PaginationLast>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
|
|
<!-- 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"
|
|
>
|
|
<input
|
|
v-model="gotoInput"
|
|
type="number"
|
|
min="1"
|
|
:max="lastPage"
|
|
inputmode="numeric"
|
|
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"
|
|
/>
|
|
<Separator orientation="vertical" class="h-full" />
|
|
<span class="text-sm text-muted-foreground">{{ lastPage }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</template>
|