[FEAT] Proj. pagination
This commit is contained in:
@@ -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, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user