[FEAT] Proj. pagination
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<!-- No Search Results -->
|
||||
<div v-else-if="filteredProjects.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div v-else-if="projects.length === 0 && searchQuery" class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-search text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
<!-- Project List -->
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="project in filteredProjects"
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-500 hover:shadow-md dark:hover:shadow-indigo-500/10 transition-all cursor-pointer overflow-hidden flex flex-col sm:flex-row h-auto sm:h-32"
|
||||
@click="selectProject(project)"
|
||||
@@ -105,9 +105,41 @@
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-400" v-if="projects.length > 0">Showing {{ filteredProjects.length }} of {{ projects.length }} project{{ projects.length !== 1 ? 's' : '' }}</p>
|
||||
<div class="flex gap-2 ml-auto">
|
||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex flex-col sm:flex-row justify-between items-center bg-white dark:bg-gray-900 gap-4 sm:gap-0">
|
||||
<div class="flex items-center gap-2 sm:gap-4 flex-wrap justify-center sm:justify-start w-full sm:w-auto">
|
||||
<p class="text-xs text-gray-400 hidden md:block" v-if="projects.length > 0">Page {{ page }} of {{ totalPages }} ({{ totalItems }} items)</p>
|
||||
|
||||
<div class="flex gap-1 items-center" v-if="totalPages > 1">
|
||||
<!-- Prev -->
|
||||
<button @click="prevPage" :disabled="page <= 1" class="w-8 h-8 flex items-center justify-center text-xs font-medium rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<template v-for="p in visiblePages" :key="p">
|
||||
<button
|
||||
v-if="p !== '...'"
|
||||
@click="goToPage(p as number)"
|
||||
:class="['w-8 h-8 flex items-center justify-center text-xs font-medium rounded transition-colors', page === p ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300']"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
<span v-else class="w-8 h-8 flex items-center justify-center text-xs text-gray-400">...</span>
|
||||
</template>
|
||||
|
||||
<!-- Next -->
|
||||
<button @click="nextPage" :disabled="page >= totalPages" class="w-8 h-8 flex items-center justify-center text-xs font-medium rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Jump -->
|
||||
<div class="flex items-center gap-2 ml-2 border-l border-gray-200 dark:border-gray-700 pl-4 hidden sm:flex" v-if="totalPages > 5">
|
||||
<span class="text-xs text-gray-400">Go to</span>
|
||||
<input type="number" min="1" :max="totalPages" class="w-12 h-8 px-2 text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded focus:ring-1 focus:ring-indigo-500 outline-none text-center" @keydown.enter="jumpToPage($event)" @blur="jumpToPage($event)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 ml-auto w-full sm:w-auto justify-center sm:justify-end">
|
||||
<button v-if="projects.length > 0" @click="loadExample" class="text-xs font-medium text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2">Load Example</button>
|
||||
<button @click="close" class="btn btn-secondary text-sm">Close</button>
|
||||
</div>
|
||||
@@ -132,23 +164,90 @@
|
||||
}>();
|
||||
|
||||
const projectStore = useProjectStore();
|
||||
const { projects, isLoading: loading } = toRefs(projectStore);
|
||||
const { createProject, loadProjectData } = useProjectManager();
|
||||
|
||||
const { createProject } = useProjectManager();
|
||||
const searchQuery = ref('');
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
if (!searchQuery.value) return projects.value;
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return projects.value.filter(p => p.name.toLowerCase().includes(query));
|
||||
const { projects, isLoading: loading, page, perPage, totalItems, totalPages, fetchProjects } = toRefs(projectStore);
|
||||
|
||||
let searchTimeout: any = null;
|
||||
|
||||
watch(searchQuery, newVal => {
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetchProjects.value(1, perPage.value, newVal);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
fetchProjects.value(page.value + 1, perPage.value, searchQuery.value);
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
fetchProjects.value(page.value - 1, perPage.value, searchQuery.value);
|
||||
}
|
||||
};
|
||||
|
||||
const goToPage = (p: number) => {
|
||||
if (p !== page.value && p >= 1 && p <= totalPages.value) {
|
||||
fetchProjects.value(p, perPage.value, searchQuery.value);
|
||||
}
|
||||
};
|
||||
|
||||
const jumpToPage = (event: any) => {
|
||||
const val = parseInt(event.target.value);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages.value) {
|
||||
goToPage(val);
|
||||
// Clear input after jump if desired, or keep it. Keeping it is fine.
|
||||
} else {
|
||||
// Reset to current page if invalid
|
||||
event.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = totalPages.value;
|
||||
const current = page.value;
|
||||
const delta = 2; // How many pages to show around current
|
||||
const range: (number | string)[] = [];
|
||||
const left = current - delta;
|
||||
const right = current + delta + 1;
|
||||
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (i === 1 || i === total || (i >= left && i < right)) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Add dots
|
||||
const finalRange: (number | string)[] = [];
|
||||
let l: number | null = null;
|
||||
|
||||
for (const i of range) {
|
||||
if (typeof i === 'number') {
|
||||
if (l) {
|
||||
if (i - l === 2) {
|
||||
finalRange.push(l + 1);
|
||||
} else if (i - l !== 1) {
|
||||
finalRange.push('...');
|
||||
}
|
||||
}
|
||||
finalRange.push(i);
|
||||
l = i;
|
||||
}
|
||||
}
|
||||
return finalRange;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
isOpen => {
|
||||
if (isOpen) {
|
||||
projectStore.fetchProjects();
|
||||
searchQuery.value = ''; // Reset search on open
|
||||
// Reset to page 1 is handled by fetchProjects default or we can explicitly call it
|
||||
projectStore.fetchProjects(1, perPage.value, '');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -15,13 +15,19 @@ export const useProjectStore = defineStore('project', () => {
|
||||
const projects = ref<Project[]>([]);
|
||||
const currentProject = ref<Project | null>(null);
|
||||
const isLoading = ref(false);
|
||||
const page = ref(1);
|
||||
const perPage = ref(12);
|
||||
const totalItems = ref(0);
|
||||
const totalPages = ref(0);
|
||||
|
||||
async function fetchProjects() {
|
||||
async function fetchProjects(pageVal = 1, perPageVal = 12, searchVal = '') {
|
||||
if (!authStore.user) return;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const records = await authStore.pb.collection('projects').getList(1, 50, {
|
||||
const filter = searchVal ? `name ~ "${searchVal}"` : '';
|
||||
const records = await authStore.pb.collection('projects').getList(pageVal, perPageVal, {
|
||||
sort: '-updated',
|
||||
filter,
|
||||
});
|
||||
projects.value = records.items.map((r: any) => ({
|
||||
id: r.id,
|
||||
@@ -30,6 +36,10 @@ export const useProjectStore = defineStore('project', () => {
|
||||
created: r.created,
|
||||
updated: r.updated,
|
||||
}));
|
||||
page.value = records.page;
|
||||
perPage.value = records.perPage;
|
||||
totalItems.value = records.totalItems;
|
||||
totalPages.value = records.totalPages;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch projects:', error);
|
||||
} finally {
|
||||
@@ -53,7 +63,8 @@ export const useProjectStore = defineStore('project', () => {
|
||||
created: record.created,
|
||||
updated: record.updated,
|
||||
};
|
||||
await fetchProjects();
|
||||
// Refresh current page
|
||||
await fetchProjects(page.value, perPage.value);
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
throw error;
|
||||
@@ -76,7 +87,9 @@ export const useProjectStore = defineStore('project', () => {
|
||||
data: record.data,
|
||||
updated: record.updated,
|
||||
};
|
||||
await fetchProjects();
|
||||
// No need to fetch all, just update locals or re-fetch current page if desired.
|
||||
// For now, let's re-fetch to keep consistency.
|
||||
await fetchProjects(page.value, perPage.value);
|
||||
} catch (error) {
|
||||
console.error('Failed to update project:', error);
|
||||
throw error;
|
||||
@@ -115,7 +128,9 @@ export const useProjectStore = defineStore('project', () => {
|
||||
if (currentProject.value?.id === id) {
|
||||
currentProject.value = null;
|
||||
}
|
||||
await fetchProjects();
|
||||
// If we delete the last item on a page, we might want to go back a page.
|
||||
// For simplicity, just refetch current page.
|
||||
await fetchProjects(page.value, perPage.value);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error);
|
||||
} finally {
|
||||
@@ -127,6 +142,10 @@ export const useProjectStore = defineStore('project', () => {
|
||||
projects,
|
||||
currentProject,
|
||||
isLoading,
|
||||
page,
|
||||
perPage,
|
||||
totalItems,
|
||||
totalPages,
|
||||
fetchProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
|
||||
Reference in New Issue
Block a user