[FEAT] Proj. pagination

This commit is contained in:
2026-01-07 19:54:09 +01:00
parent fa23980917
commit cdb86452c3
2 changed files with 136 additions and 18 deletions

View File

@@ -40,7 +40,7 @@
</div> </div>
<!-- No Search Results --> <!-- 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"> <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> <i class="fas fa-search text-2xl text-gray-400 dark:text-gray-500"></i>
</div> </div>
@@ -51,7 +51,7 @@
<!-- Project List --> <!-- Project List -->
<div v-else class="flex flex-col gap-3"> <div v-else class="flex flex-col gap-3">
<div <div
v-for="project in filteredProjects" v-for="project in projects"
:key="project.id" :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" 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)" @click="selectProject(project)"
@@ -105,9 +105,41 @@
</div> </div>
<!-- Footer --> <!-- 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"> <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">
<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 items-center gap-2 sm:gap-4 flex-wrap justify-center sm:justify-start w-full sm:w-auto">
<div class="flex gap-2 ml-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 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> <button @click="close" class="btn btn-secondary text-sm">Close</button>
</div> </div>
@@ -132,23 +164,90 @@
}>(); }>();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
const { projects, isLoading: loading } = toRefs(projectStore); const { createProject } = useProjectManager();
const { createProject, loadProjectData } = useProjectManager();
const searchQuery = ref(''); const searchQuery = ref('');
const filteredProjects = computed(() => { const { projects, isLoading: loading, page, perPage, totalItems, totalPages, fetchProjects } = toRefs(projectStore);
if (!searchQuery.value) return projects.value;
const query = searchQuery.value.toLowerCase(); let searchTimeout: any = null;
return projects.value.filter(p => p.name.toLowerCase().includes(query));
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( watch(
() => props.isOpen, () => props.isOpen,
isOpen => { isOpen => {
if (isOpen) { if (isOpen) {
projectStore.fetchProjects();
searchQuery.value = ''; // Reset search on open 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, '');
} }
} }
); );

View File

@@ -15,13 +15,19 @@ export const useProjectStore = defineStore('project', () => {
const projects = ref<Project[]>([]); const projects = ref<Project[]>([]);
const currentProject = ref<Project | null>(null); const currentProject = ref<Project | null>(null);
const isLoading = ref(false); 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; if (!authStore.user) return;
isLoading.value = true; isLoading.value = true;
try { 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', sort: '-updated',
filter,
}); });
projects.value = records.items.map((r: any) => ({ projects.value = records.items.map((r: any) => ({
id: r.id, id: r.id,
@@ -30,6 +36,10 @@ export const useProjectStore = defineStore('project', () => {
created: r.created, created: r.created,
updated: r.updated, updated: r.updated,
})); }));
page.value = records.page;
perPage.value = records.perPage;
totalItems.value = records.totalItems;
totalPages.value = records.totalPages;
} catch (error) { } catch (error) {
console.error('Failed to fetch projects:', error); console.error('Failed to fetch projects:', error);
} finally { } finally {
@@ -53,7 +63,8 @@ export const useProjectStore = defineStore('project', () => {
created: record.created, created: record.created,
updated: record.updated, updated: record.updated,
}; };
await fetchProjects(); // Refresh current page
await fetchProjects(page.value, perPage.value);
} catch (error) { } catch (error) {
console.error('Failed to create project:', error); console.error('Failed to create project:', error);
throw error; throw error;
@@ -76,7 +87,9 @@ export const useProjectStore = defineStore('project', () => {
data: record.data, data: record.data,
updated: record.updated, 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) { } catch (error) {
console.error('Failed to update project:', error); console.error('Failed to update project:', error);
throw error; throw error;
@@ -115,7 +128,9 @@ export const useProjectStore = defineStore('project', () => {
if (currentProject.value?.id === id) { if (currentProject.value?.id === id) {
currentProject.value = null; 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) { } catch (error) {
console.error('Failed to delete project:', error); console.error('Failed to delete project:', error);
} finally { } finally {
@@ -127,6 +142,10 @@ export const useProjectStore = defineStore('project', () => {
projects, projects,
currentProject, currentProject,
isLoading, isLoading,
page,
perPage,
totalItems,
totalPages,
fetchProjects, fetchProjects,
createProject, createProject,
updateProject, updateProject,