Skip to content

Commit a17d956

Browse files
committed
feat: add reusable Pagination components with enhanced inventory UI
1 parent c95cd05 commit a17d956

5 files changed

Lines changed: 142 additions & 11 deletions

File tree

app/Http/Controllers/Admin/InventoryController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function __construct(
2424
*/
2525
public function index()
2626
{
27-
$items = $this->inventoryService->getAll();
27+
$items = $this->inventoryService->getPaginated();
2828

2929
return Inertia::render('admin/Inventory/Index', [
3030
'items' => $items

app/Services/InventoryService.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ public function __construct(
1919
) {}
2020

2121
/**
22-
* Get all inventory items
22+
* Get paginated inventory items
2323
*
24-
* @return \Illuminate\Database\Eloquent\Collection
24+
* @param int $perPage
25+
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
2526
*/
26-
public function getAll()
27+
public function getPaginated(int $perPage = 10)
2728
{
28-
return $this->inventoryRepo->all();
29+
return $this->inventoryRepo->paginate($perPage);
2930
}
3031

3132
/**
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script setup lang="ts">
2+
import { Link } from '@inertiajs/vue3';
3+
import { Button } from '@/components/ui/button';
4+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-vue-next';
5+
import { PaginationData, PaginationLink } from '@/types';
6+
7+
interface Props {
8+
pagination: PaginationData;
9+
showInfo?: boolean;
10+
}
11+
12+
const props = withDefaults(defineProps<Props>(), {
13+
showInfo: true,
14+
});
15+
16+
const getPageNumber = (label: string): number | null => {
17+
const num = parseInt(label);
18+
return isNaN(num) ? null : num;
19+
}
20+
21+
// Remove query parameters from URL
22+
const removeQueryParams = (url: string | null): string | null => {
23+
if (!url) return null;
24+
return url.split('?')[0];
25+
}
26+
27+
// Get first page URL (without query params)
28+
const getFirstPageUrl = (): string | null => {
29+
const firstPageLink = props.pagination.links.find(link => getPageNumber(link.label) === 1);
30+
return firstPageLink?.url ? removeQueryParams(firstPageLink.url) : null;
31+
}
32+
33+
// Get last page URL
34+
const getLastPageUrl = (): string | null => {
35+
const lastPageLink = props.pagination.links.find(link => getPageNumber(link.label) === props.pagination.last_page);
36+
return lastPageLink?.url || null;
37+
}
38+
39+
// Get previous page URL (first link in array)
40+
const getPreviousPageUrl = (): string | null => {
41+
return props.pagination.links[0]?.url || null;
42+
}
43+
44+
// Get next page URL (last link in array)
45+
const getNextPageUrl = (): string | null => {
46+
return props.pagination.links[props.pagination.links.length - 1]?.url || null;
47+
}
48+
49+
// Get page URL, remove query params if it's page 1
50+
const getPageUrl = (link: PaginationLink): string | null => {
51+
if (!link.url) return null;
52+
const pageNum = getPageNumber(link.label);
53+
return pageNum === 1 ? removeQueryParams(link.url) : link.url;
54+
}
55+
</script>
56+
57+
<template>
58+
<div class="flex items-center justify-between px-2 py-4">
59+
<!-- Pagination Info -->
60+
<div v-if="showInfo" class="text-sm text-muted-foreground">
61+
Showing <span class="font-medium">{{ pagination.from }}</span> to
62+
<span class="font-medium">{{ pagination.to }}</span> of
63+
<span class="font-medium">{{ pagination.total }}</span> results
64+
</div>
65+
<div v-else></div>
66+
67+
<!-- Pagination Controls -->
68+
<div class="flex items-center gap-2">
69+
<!-- First Page -->
70+
<Button :as="Link" :href="getFirstPageUrl() || '#'" :disabled="pagination.current_page === 1"
71+
variant="outline" size="icon" class="h-8 w-8"
72+
:class="{ 'pointer-events-none opacity-50': pagination.current_page === 1 }">
73+
<ChevronsLeft :size="16" />
74+
</Button>
75+
76+
<!-- Previous Page -->
77+
<Button :as="Link" :href="getPreviousPageUrl() || '#'" :disabled="pagination.current_page === 1"
78+
variant="outline" size="icon" class="h-8 w-8"
79+
:class="{ 'pointer-events-none opacity-50': pagination.current_page === 1 }">
80+
<ChevronLeft :size="16" />
81+
</Button>
82+
83+
<!-- Page Numbers -->
84+
<template v-for="(link, index) in pagination.links" :key="index">
85+
<Button v-if="getPageNumber(link.label) !== null" :as="Link" :href="getPageUrl(link) || '#'"
86+
:variant="link.active ? 'default' : 'outline'" size="icon" class="h-8 w-8"
87+
:class="{ 'pointer-events-none': !link.url }">
88+
{{ link.label }}
89+
</Button>
90+
</template>
91+
92+
<!-- Next Page -->
93+
<Button :as="Link" :href="getNextPageUrl() || '#'"
94+
:disabled="pagination.current_page === pagination.last_page" variant="outline" size="icon"
95+
class="h-8 w-8"
96+
:class="{ 'pointer-events-none opacity-50': pagination.current_page === pagination.last_page }">
97+
<ChevronRight :size="16" />
98+
</Button>
99+
100+
<!-- Last Page -->
101+
<Button :as="Link" :href="getLastPageUrl() || '#'"
102+
:disabled="pagination.current_page === pagination.last_page" variant="outline" size="icon"
103+
class="h-8 w-8"
104+
:class="{ 'pointer-events-none opacity-50': pagination.current_page === pagination.last_page }">
105+
<ChevronsRight :size="16" />
106+
</Button>
107+
</div>
108+
</div>
109+
</template>

resources/js/pages/admin/Inventory/Index.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script setup lang="ts">
22
import AppLayout from '@/layouts/AppLayout.vue';
3-
import { type BreadcrumbItem } from '@/types';
3+
import { PaginationData, type BreadcrumbItem } from '@/types';
44
import { Head, router } from '@inertiajs/vue3';
55
import inventory from '@/routes/admin/inventory';
66
import LinkButton from '@/components/LinkButton.vue';
77
import DataTable from '@/components/DataTable.vue';
88
import type { DataTableColumn, DataTableAction, InventoryItem } from '@/types/admin';
99
import { Eye, Pencil, Trash2 } from 'lucide-vue-next';
10+
import Pagination from '@/components/Pagination.vue';
1011
1112
const breadcrumbs: BreadcrumbItem[] = [
1213
{
@@ -16,12 +17,12 @@ const breadcrumbs: BreadcrumbItem[] = [
1617
];
1718
1819
interface Props {
19-
items?: InventoryItem[];
20+
items: PaginationData & {
21+
data: InventoryItem[];
22+
};
2023
}
2124
22-
const props = withDefaults(defineProps<Props>(), {
23-
items: () => [],
24-
});
25+
const props = defineProps<Props>();
2526
2627
const columns: DataTableColumn<InventoryItem>[] = [
2728
{
@@ -105,7 +106,8 @@ const actions: DataTableAction<InventoryItem>[] = [
105106
<LinkButton :href="inventory.create().url" label="Add an Item" />
106107
</div>
107108

108-
<DataTable :data="items" :columns="columns" :actions="actions"
109+
<!-- Inventory Table -->
110+
<DataTable :data="items.data" :columns="columns" :actions="actions"
109111
empty-message="No inventory items found. Click 'Add an Item' to get started.">
110112

111113
<!-- Item name -->
@@ -143,6 +145,9 @@ const actions: DataTableAction<InventoryItem>[] = [
143145
</span>
144146
</template>
145147
</DataTable>
148+
149+
<!-- Pagination -->
150+
<Pagination :pagination="items" />
146151
</div>
147152
</AppLayout>
148153
</template>

resources/js/types/index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,19 @@ export interface User {
4040
}
4141

4242
export type BreadcrumbItemType = BreadcrumbItem;
43+
44+
export interface PaginationLink {
45+
url: string | null;
46+
label: string;
47+
active: boolean;
48+
}
49+
50+
export interface PaginationData {
51+
current_page: number;
52+
last_page: number;
53+
per_page: number;
54+
total: number;
55+
from: number;
56+
to: number;
57+
links: PaginationLink[];
58+
}

0 commit comments

Comments
 (0)