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:
parent
a9b4510d08
commit
10f777f3cc
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user