feat: add pagination
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
41
lib/api.ts
41
lib/api.ts
@@ -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 => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user