Sprint C#15-16: paginate Approve Identified API/UI and in-app photo preview.

This commit is contained in:
2026-08-04 22:03:35 -04:00
parent a2a3c7e9c2
commit 6d9e270ac2
3 changed files with 127 additions and 53 deletions
@@ -61,10 +61,22 @@ export interface ClearDatabaseResponse {
}
export const pendingIdentificationsApi = {
list: async (includeDenied: boolean = false): Promise<PendingIdentificationsListResponse> => {
list: async (
includeDenied: boolean = false,
options?: { page?: number; pageSize?: number }
): Promise<PendingIdentificationsListResponse> => {
const params: Record<string, string | number | boolean> = {
include_denied: includeDenied,
}
if (options?.page !== undefined) {
params.page = options.page
}
if (options?.pageSize !== undefined) {
params.page_size = options.pageSize
}
const res = await apiClient.get<PendingIdentificationsListResponse>(
'/api/v1/pending-identifications',
{ params: { include_denied: includeDenied } }
{ params }
)
return res.data
},
+70 -5
View File
@@ -26,13 +26,21 @@ export default function ApproveIdentified() {
const [dateFrom, setDateFrom] = useState<string>('')
const [dateTo, setDateTo] = useState<string>('')
const [clearing, setClearing] = useState(false)
const [page, setPage] = useState(1)
const [pageSize] = useState(25)
const [total, setTotal] = useState(0)
const [previewPhotoUrl, setPreviewPhotoUrl] = useState<string | null>(null)
const loadPendingIdentifications = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await pendingIdentificationsApi.list(includeDenied)
const response = await pendingIdentificationsApi.list(includeDenied, {
page,
pageSize,
})
setPendingIdentifications(response.items)
setTotal(response.total)
} catch (err: any) {
let errorMessage = 'Failed to load pending identifications'
if (err.response?.data?.detail) {
@@ -49,6 +57,10 @@ export default function ApproveIdentified() {
} finally {
setLoading(false)
}
}, [includeDenied, page, pageSize])
useEffect(() => {
setPage(1)
}, [includeDenied])
useEffect(() => {
@@ -284,7 +296,12 @@ export default function ApproveIdentified() {
<>
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600">
Total pending identifications: <span className="font-semibold">{pendingIdentifications.length}</span>
Total pending identifications: <span className="font-semibold">{total}</span>
{total > 0 && (
<span className="ml-2 text-gray-500">
(page {page} of {Math.max(1, Math.ceil(total / pageSize))})
</span>
)}
</div>
<div className="flex items-center gap-4">
<button
@@ -458,10 +475,11 @@ export default function ApproveIdentified() {
<div
className="cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => {
const photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${pending.photo_id}/image`
window.open(photoUrl, '_blank')
setPreviewPhotoUrl(
`${apiClient.defaults.baseURL}/api/v1/photos/${pending.photo_id}/image`
)
}}
title="Click to open full photo"
title="Click to preview full photo"
>
<img
src={`${apiClient.defaults.baseURL}/api/v1/faces/${pending.face_id}/crop`}
@@ -571,10 +589,57 @@ export default function ApproveIdentified() {
</table>
</div>
)}
{total > pageSize && (
<div className="mt-4 flex items-center justify-between">
<button
type="button"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1 || loading}
className="px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
>
Previous page
</button>
<span className="text-sm text-gray-600">
Showing {(page - 1) * pageSize + 1}{Math.min(page * pageSize, total)} of {total}
</span>
<button
type="button"
onClick={() => setPage((p) => p + 1)}
disabled={page * pageSize >= total || loading}
className="px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
>
Next page
</button>
</div>
)}
</>
)}
</div>
{previewPhotoUrl && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
role="dialog"
aria-modal="true"
aria-label="Photo preview"
onClick={() => setPreviewPhotoUrl(null)}
>
<button
type="button"
className="absolute right-4 top-4 rounded bg-white/90 px-3 py-1 text-sm font-medium text-gray-900"
onClick={() => setPreviewPhotoUrl(null)}
>
Close
</button>
<img
src={previewPhotoUrl}
alt="Full photo preview"
className="max-h-[90vh] max-w-full rounded object-contain"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
{/* Report Modal */}
{showReport && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">