feat: add pagination

This commit is contained in:
2026-03-10 15:28:53 +05:30
parent 58794fc40e
commit 9bdbd058e7
5 changed files with 140 additions and 30 deletions

View File

@@ -39,6 +39,10 @@ export default function CreateLocationPage() {
const [loadingProjects, setLoadingProjects] = useState(false) const [loadingProjects, setLoadingProjects] = useState(false)
const [loadingPackages, setLoadingPackages] = useState(false) const [loadingPackages, setLoadingPackages] = useState(false)
// Pagination state
const [skip, setSkip] = useState(0)
const [limit, setLimit] = useState(10)
// Editing state // Editing state
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [currentLocation, setCurrentLocation] = useState<Location | null>(null) const [currentLocation, setCurrentLocation] = useState<Location | null>(null)
@@ -55,11 +59,11 @@ export default function CreateLocationPage() {
const [endLng, setEndLng] = useState("") const [endLng, setEndLng] = useState("")
// Load locations and projects // Load locations and projects
const loadLocations = async () => { const loadLocations = async (currentSkip = skip, currentLimit = limit) => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchAllLocations() const data = await fetchAllLocations({ skip: currentSkip, limit: currentLimit })
setLocations(data) setLocations(data)
} catch (err) { } catch (err) {
setError("Failed to load locations. Please check if the backend is running.") setError("Failed to load locations. Please check if the backend is running.")
@@ -71,7 +75,7 @@ export default function CreateLocationPage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setLoadingProjects(true) setLoadingProjects(true)
const data = await fetchProjects() const data = await fetchProjects({ skip: 0, limit: 1000 })
setProjects(data) setProjects(data)
} catch (err) { } catch (err) {
setError("Failed to load projects.") setError("Failed to load projects.")
@@ -82,7 +86,7 @@ export default function CreateLocationPage() {
const loadAllPackages = async () => { const loadAllPackages = async () => {
try { try {
const data = await fetchAllPackages() const data = await fetchAllPackages({ skip: 0, limit: 1000 })
setAllPackages(data) setAllPackages(data)
} catch (err) { } catch (err) {
console.error("Failed to load all packages") console.error("Failed to load all packages")
@@ -90,10 +94,10 @@ export default function CreateLocationPage() {
} }
useEffect(() => { useEffect(() => {
loadLocations() loadLocations(skip, limit)
loadProjects() loadProjects()
loadAllPackages() loadAllPackages()
}, []) }, [skip, limit])
// Load packages when project changes // Load packages when project changes
useEffect(() => { useEffect(() => {
@@ -107,7 +111,7 @@ export default function CreateLocationPage() {
try { try {
setLoadingPackages(true) setLoadingPackages(true)
if (!isEditing) setSelectedPackageId("") if (!isEditing) setSelectedPackageId("")
const data = await fetchPackagesByProject(selectedProjectId) const data = await fetchPackagesByProject(selectedProjectId, { skip: 0, limit: 1000 })
setPackages(data) setPackages(data)
} catch (err) { } catch (err) {
setError("Failed to load packages for the selected project.") setError("Failed to load packages for the selected project.")
@@ -325,6 +329,15 @@ export default function CreateLocationPage() {
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Location" addButtonText="Add New Location"
isLoading={isLoading} isLoading={isLoading}
pagination={{
skip,
limit,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
}
}}
/> />
</div> </div>

View File

@@ -33,6 +33,10 @@ export default function CreatePackagePage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [loadingProjects, setLoadingProjects] = useState(false) const [loadingProjects, setLoadingProjects] = useState(false)
// Pagination state
const [skip, setSkip] = useState(0)
const [limit, setLimit] = useState(10)
// Editing state // Editing state
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [currentPackage, setCurrentPackage] = useState<PackageType | null>(null) const [currentPackage, setCurrentPackage] = useState<PackageType | null>(null)
@@ -43,11 +47,11 @@ export default function CreatePackagePage() {
const [region, setRegion] = useState("") const [region, setRegion] = useState("")
// Load packages and projects // Load packages and projects
const loadPackages = async () => { const loadPackages = async (currentSkip = skip, currentLimit = limit) => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchAllPackages() const data = await fetchAllPackages({ skip: currentSkip, limit: currentLimit })
setPackages(data) setPackages(data)
} catch (err) { } catch (err) {
setError("Failed to load packages. Please check if the backend is running.") setError("Failed to load packages. Please check if the backend is running.")
@@ -59,7 +63,7 @@ export default function CreatePackagePage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
setLoadingProjects(true) setLoadingProjects(true)
const data = await fetchProjects() const data = await fetchProjects({ skip: 0, limit: 1000 }) // Load all projects for selector
setProjects(data) setProjects(data)
} catch (err) { } catch (err) {
setError("Failed to load projects.") setError("Failed to load projects.")
@@ -69,9 +73,9 @@ export default function CreatePackagePage() {
} }
useEffect(() => { useEffect(() => {
loadPackages() loadPackages(skip, limit)
loadProjects() loadProjects()
}, []) }, [skip, limit])
const resetForm = () => { const resetForm = () => {
setSelectedProjectId("") setSelectedProjectId("")
@@ -229,6 +233,15 @@ export default function CreatePackagePage() {
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Package" addButtonText="Add New Package"
isLoading={isLoading} isLoading={isLoading}
pagination={{
skip,
limit,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
}
}}
/> />
</div> </div>

View File

@@ -20,6 +20,10 @@ export default function CreateProjectPage() {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// Pagination state
const [skip, setSkip] = useState(0)
const [limit, setLimit] = useState(10)
// Editing state // Editing state
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [currentProject, setCurrentProject] = useState<Project | null>(null) const [currentProject, setCurrentProject] = useState<Project | null>(null)
@@ -34,11 +38,11 @@ export default function CreateProjectPage() {
const [endLng, setEndLng] = useState("") const [endLng, setEndLng] = useState("")
// Load projects // Load projects
const loadProjects = async () => { const loadProjects = async (currentSkip = skip, currentLimit = limit) => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
const data = await fetchProjects() const data = await fetchProjects({ skip: currentSkip, limit: currentLimit })
setProjects(data) setProjects(data)
} catch (err) { } catch (err) {
setError("Failed to load projects. Please check if the backend is running.") setError("Failed to load projects. Please check if the backend is running.")
@@ -48,8 +52,8 @@ export default function CreateProjectPage() {
} }
useEffect(() => { useEffect(() => {
loadProjects() loadProjects(skip, limit)
}, []) }, [skip, limit])
const resetForm = () => { const resetForm = () => {
setName("") setName("")
@@ -210,6 +214,15 @@ export default function CreateProjectPage() {
onDelete={handleDelete} onDelete={handleDelete}
addButtonText="Add New Project" addButtonText="Add New Project"
isLoading={isLoading} isLoading={isLoading}
pagination={{
skip,
limit,
onPageChange: setSkip,
onLimitChange: (newLimit) => {
setLimit(newLimit);
setSkip(0); // Reset skip when limit changes
}
}}
/> />
</div> </div>

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { Plus, Edit2, Trash2 } from "lucide-react" import { Plus, Edit2, Trash2, ChevronLeft, ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Tooltip, Tooltip,
@@ -25,6 +25,13 @@ interface DataTableProps<T> {
isLoading?: boolean isLoading?: boolean
onEdit?: (item: T) => void onEdit?: (item: T) => void
onDelete?: (item: T) => void onDelete?: (item: T) => void
pagination?: {
skip: number
limit: number
totalItems?: number
onPageChange: (newSkip: number) => void
onLimitChange: (newLimit: number) => void
}
} }
export function DataTable<T extends Record<string, any>>({ export function DataTable<T extends Record<string, any>>({
@@ -35,7 +42,8 @@ export function DataTable<T extends Record<string, any>>({
addButtonText, addButtonText,
isLoading = false, isLoading = false,
onEdit, onEdit,
onDelete onDelete,
pagination
}: DataTableProps<T>) { }: DataTableProps<T>) {
const showActions = !!onEdit || !!onDelete; const showActions = !!onEdit || !!onDelete;
const totalCols = columns.length + (showActions ? 1 : 0); const totalCols = columns.length + (showActions ? 1 : 0);
@@ -171,6 +179,52 @@ export function DataTable<T extends Record<string, any>>({
</table> </table>
</div> </div>
</div> </div>
{/* Pagination Controls */}
{pagination && (
<div className="flex items-center justify-between px-4 py-3 bg-white/40 backdrop-blur-md rounded-b-lg border-t border-gray-200 mt-1">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-500">Rows per page:</span>
<select
value={pagination.limit}
onChange={(e) => pagination.onLimitChange(Number(e.target.value))}
className="bg-transparent text-xs font-bold text-gray-700 outline-none cursor-pointer focus:ring-0"
>
{[10, 25, 50, 100].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<span className="text-xs text-gray-500">
Showing {pagination.skip + 1} - {pagination.skip + data.length}
{pagination.totalItems !== undefined && ` of ${pagination.totalItems}`}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
disabled={pagination.skip === 0 || isLoading}
onClick={() => pagination.onPageChange(Math.max(0, pagination.skip - pagination.limit))}
className="h-8 w-8 p-0 rounded-md hover:bg-blue-50 text-gray-600 disabled:opacity-30"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
disabled={data.length < pagination.limit || isLoading}
onClick={() => pagination.onPageChange(pagination.skip + pagination.limit)}
className="h-8 w-8 p-0 rounded-md hover:bg-blue-50 text-gray-600 disabled:opacity-30"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -43,6 +43,11 @@ export interface Location {
updated_at: string updated_at: string
} }
export interface PaginationParams {
skip?: number;
limit?: number;
}
// Helper function for GET API requests // Helper function for GET API requests
async function apiRequest<T>(endpoint: string): Promise<T> { async function apiRequest<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, { const response = await fetch(`${API_URL}${endpoint}`, {
@@ -241,36 +246,46 @@ export async function deleteLocation(locationId: string): Promise<{ message: str
/** /**
* Fetch all projects * Fetch all projects
*/ */
export async function fetchProjects(): Promise<Project[]> { export async function fetchProjects(params?: PaginationParams): Promise<Project[]> {
return apiRequest<Project[]>("/projects/") const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<Project[]>(`/projects/?skip=${skip}&limit=${limit}`)
} }
/** /**
* Fetch packages filtered by project ID * Fetch packages filtered by project ID
*/ */
export async function fetchPackagesByProject(projectId: string): Promise<Package[]> { export async function fetchPackagesByProject(projectId: string, params?: PaginationParams): Promise<Package[]> {
return apiRequest<Package[]>(`/packages/?project_id=${projectId}`) const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<Package[]>(`/packages/?project_id=${projectId}&skip=${skip}&limit=${limit}`)
} }
/** /**
* Fetch locations filtered by package ID * Fetch locations filtered by package ID
*/ */
export async function fetchLocationsByPackage(packageId: string): Promise<Location[]> { export async function fetchLocationsByPackage(packageId: string, params?: PaginationParams): Promise<Location[]> {
return apiRequest<Location[]>(`/locations/?package_id=${packageId}`) const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<Location[]>(`/locations/?package_id=${packageId}&skip=${skip}&limit=${limit}`)
} }
/** /**
* Fetch all packages (for dashboard) * Fetch all packages (for dashboard)
*/ */
export async function fetchAllPackages(): Promise<Package[]> { export async function fetchAllPackages(params?: PaginationParams): Promise<Package[]> {
return apiRequest<Package[]>("/packages/") const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<Package[]>(`/packages/?skip=${skip}&limit=${limit}`)
} }
/** /**
* Fetch all locations (for dashboard) * Fetch all locations (for dashboard)
*/ */
export async function fetchAllLocations(): Promise<Location[]> { export async function fetchAllLocations(params?: PaginationParams): Promise<Location[]> {
return apiRequest<Location[]>("/locations/") const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
return apiRequest<Location[]>(`/locations/?skip=${skip}&limit=${limit}`)
} }
// Video type for dashboard // Video type for dashboard
@@ -293,7 +308,9 @@ export interface Video {
/** /**
* Fetch all videos (for dashboard) * Fetch all videos (for dashboard)
*/ */
export async function fetchVideos(): Promise<Video[]> { export async function fetchVideos(params?: PaginationParams): Promise<Video[]> {
const skip = params?.skip ?? 0
const limit = params?.limit ?? 100
const response = await apiRequest<{ const response = await apiRequest<{
videos: Array<{ videos: Array<{
video_id: string; video_id: string;
@@ -309,7 +326,7 @@ export async function fetchVideos(): Promise<Video[]> {
total_detections?: number; total_detections?: number;
} }
}> }>
}>("/videos") }>(`/videos?skip=${skip}&limit=${limit}`)
// Transform the response to match our Video interface // Transform the response to match our Video interface
return response.videos.map(v => ({ return response.videos.map(v => ({