feat: Add pagination and setup area toggle in Identify component for improved navigation

This commit introduces pagination controls in the Identify component, allowing users to navigate through faces more efficiently with "Previous" and "Next" buttons. Additionally, a setup area toggle is added to collapse or expand the filters section, enhancing the user interface. The state management for the current page is updated to persist across sessions, improving overall user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09 2025-12-11 11:42:23 -05:00
parent a9b4510d08
commit 10f777f3cc

View File

@ -20,6 +20,7 @@ export default function Identify() {
const [searchParams, setSearchParams] = useSearchParams()
const [faces, setFaces] = useState<FaceItem[]>([])
const [, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(50)
const [minQuality, setMinQuality] = useState(0.0)
const [sortBy, setSortBy] = useState<SortBy>('quality')
@ -63,6 +64,7 @@ export default function Identify() {
const [busy, setBusy] = useState(false)
const [imageLoading, setImageLoading] = useState(false)
const [filtersCollapsed, setFiltersCollapsed] = useState(true)
const [setupAreaCollapsed, setSetupAreaCollapsed] = useState(false)
const [loadingFaces, setLoadingFaces] = useState(false)
const [availableTags, setAvailableTags] = useState<TagResponse[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([])
@ -134,8 +136,13 @@ export default function Identify() {
return Boolean(firstName.trim() && lastName.trim())
}, [personId, firstName, lastName, currentFace])
const loadFaces = async (clearState: boolean = false, ignorePhotoIds: boolean = false) => {
const loadFaces = async (
clearState: boolean = false,
ignorePhotoIds: boolean = false,
targetPage?: number
) => {
setLoadingFaces(true)
const pageToLoad = targetPage ?? page
try {
// Clear saved state if explicitly requested (Refresh button)
@ -144,7 +151,7 @@ export default function Identify() {
}
const res = await facesApi.getUnidentified({
page: 1,
page: pageToLoad,
page_size: pageSize,
min_quality: minQuality,
date_taken_from: dateFrom || undefined,
@ -167,6 +174,7 @@ export default function Identify() {
setFaces(filtered)
setTotal(filtered.length)
setPage(pageToLoad)
setCurrentIdx(0)
// Clear form data when refreshing
if (clearState) {
@ -408,6 +416,9 @@ export default function Identify() {
if (state.selectedSimilar && typeof state.selectedSimilar === 'object') {
setSelectedSimilar(state.selectedSimilar)
}
if (state.page !== undefined) {
setPage(state.page)
}
// Mark that we restored state, so we don't reload
initialLoadRef.current = true
// Mark restoration as complete after state is restored
@ -436,12 +447,13 @@ export default function Identify() {
similar,
faceFormData,
selectedSimilar,
page,
}
sessionStorage.setItem(STATE_KEY, JSON.stringify(state))
} catch (error) {
console.error('Error saving state to sessionStorage:', error)
}
}, [faces, currentIdx, similar, faceFormData, selectedSimilar, stateRestored])
}, [faces, currentIdx, similar, faceFormData, selectedSimilar, page, stateRestored])
// Save state on unmount (when navigating away) - use refs to capture latest values
const facesRef = useRef(faces)
@ -449,6 +461,7 @@ export default function Identify() {
const similarRef = useRef(similar)
const faceFormDataRef = useRef(faceFormData)
const selectedSimilarRef = useRef(selectedSimilar)
const pageRef = useRef(page)
// Update refs whenever state changes
useEffect(() => {
@ -457,7 +470,8 @@ export default function Identify() {
similarRef.current = similar
faceFormDataRef.current = faceFormData
selectedSimilarRef.current = selectedSimilar
}, [faces, currentIdx, similar, faceFormData, selectedSimilar])
pageRef.current = page
}, [faces, currentIdx, similar, faceFormData, selectedSimilar, page])
// Save state on unmount (when navigating away)
useEffect(() => {
@ -469,6 +483,7 @@ export default function Identify() {
similar: similarRef.current,
faceFormData: faceFormDataRef.current,
selectedSimilar: selectedSimilarRef.current,
page: pageRef.current,
}
sessionStorage.setItem(STATE_KEY, JSON.stringify(state))
} catch (error) {
@ -503,7 +518,8 @@ export default function Identify() {
// Reload faces when includeExcludedFaces changes
useEffect(() => {
if (settingsLoaded && !photoIds) {
loadFaces(false)
setPage(1)
loadFaces(false, false, 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [includeExcludedFaces])
@ -558,7 +574,8 @@ export default function Identify() {
// But only if restoration is complete (prevents reload during initial restoration)
useEffect(() => {
if (initialLoadRef.current && restorationCompleteRef.current) {
loadFaces()
setPage(1)
loadFaces(false, false, 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uniqueFacesOnly])
@ -567,7 +584,8 @@ export default function Identify() {
// But only if restoration is complete (prevents reload during initial restoration)
useEffect(() => {
if (initialLoadRef.current && restorationCompleteRef.current) {
loadFaces()
setPage(1)
loadFaces(false, false, 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageSize])
@ -733,6 +751,19 @@ export default function Identify() {
return `Face ${currentIdx + 1} of ${faces.length}`
}, [currentFace, currentIdx, faces.length])
const handleNextPage = () => {
if (loadingFaces) return
const nextPage = page + 1
setPage(nextPage)
loadFaces(false, false, nextPage)
}
const handlePrevPage = () => {
if (loadingFaces || page <= 1) return
const prevPage = Math.max(1, page - 1)
setPage(prevPage)
loadFaces(false, false, prevPage)
}
// Toggle excluded status for current face
const toggleBlockFace = useCallback(async () => {
if (!currentFace) return
@ -957,6 +988,7 @@ export default function Identify() {
{/* Left: Controls and current face */}
<div className="col-span-4">
{/* Unique Faces Checkbox and Batch Size - Outside Filters */}
{!setupAreaCollapsed && (
<div className="bg-white rounded-lg shadow mb-4 p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex-1">
@ -983,8 +1015,29 @@ export default function Identify() {
</select>
</div>
</div>
<div className="flex items-center justify-between gap-3 mt-4">
<div className="text-sm text-gray-600">Page {page}</div>
<div className="flex gap-2">
<button
onClick={handlePrevPage}
disabled={loadingFaces || page <= 1}
className="px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed text-sm font-medium"
>
Prev {pageSize}
</button>
<button
onClick={handleNextPage}
disabled={loadingFaces}
className="px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed text-sm font-medium"
>
{loadingFaces ? 'Loading...' : `Next ${pageSize}`}
</button>
</div>
</div>
</div>
)}
{!setupAreaCollapsed && (
<div className="bg-white rounded-lg shadow mb-4">
<div className="flex items-center justify-between p-4 border-b cursor-pointer hover:bg-gray-50" onClick={() => setFiltersCollapsed(!filtersCollapsed)}>
<h2 className="text-lg font-semibold text-gray-900">Filters</h2>
@ -1108,7 +1161,10 @@ export default function Identify() {
<div className="mt-4 pt-3 border-t">
<div className="flex gap-2">
<button
onClick={() => loadFaces(false)}
onClick={() => {
setPage(1)
loadFaces(false, false, 1)
}}
disabled={loadingFaces}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium"
>
@ -1118,7 +1174,35 @@ export default function Identify() {
</div>
</div>
)}
{/* Collapse button at bottom of filters section */}
<div className="border-t p-2 flex justify-center">
<button
onClick={() => setSetupAreaCollapsed(true)}
className="text-gray-500 hover:text-gray-700 focus:outline-none flex items-center gap-1 text-sm"
aria-label="Hide setup and filters"
title="Hide setup and filters"
>
<span></span>
<span>Hide</span>
</button>
</div>
</div>
)}
{/* Show expand button when collapsed */}
{setupAreaCollapsed && (
<div className="bg-white rounded-lg shadow mb-4 p-2 flex justify-center">
<button
onClick={() => setSetupAreaCollapsed(false)}
className="text-gray-500 hover:text-gray-700 focus:outline-none flex items-center gap-1 text-sm"
aria-label="Show setup and filters"
title="Show setup and filters"
>
<span></span>
<span>Show Setup & Filters</span>
</button>
</div>
)}
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-2">