feat: Add processed and unprocessed photo search options and sessionStorage management

This commit introduces new search options for processed and unprocessed photos in the Search component, enhancing the photo management capabilities. The Identify component has been updated to clear sessionStorage settings on logout and authentication failure, improving user experience by ensuring a clean state. Additionally, the API has been modified to support these new search parameters, ensuring seamless integration with the frontend. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-11-11 13:45:43 -05:00
parent 85dd6a68b3
commit 20f1a4207f
9 changed files with 138 additions and 37 deletions
+2
View File
@@ -25,6 +25,8 @@ apiClient.interceptors.response.use(
if (error.response?.status === 401) {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
// Clear sessionStorage settings on authentication failure
sessionStorage.removeItem('identify_settings')
window.location.href = '/login'
}
return Promise.reject(error)
+1 -1
View File
@@ -73,7 +73,7 @@ export const photosApi = {
},
searchPhotos: async (params: {
search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags'
search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed'
person_name?: string
tag_names?: string
match_all?: boolean
+2
View File
@@ -74,6 +74,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const logout = () => {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
// Clear sessionStorage settings on logout
sessionStorage.removeItem('identify_settings')
setAuthState({
isAuthenticated: false,
username: null,
+19 -14
View File
@@ -18,7 +18,8 @@ export default function Identify() {
const [sortDir, setSortDir] = useState<SortDir>('desc')
const [dateFrom, setDateFrom] = useState<string>('')
const [dateTo, setDateTo] = useState<string>('')
const [dateProcessed, setDateProcessed] = useState<string>('')
// dateProcessed filter is hidden, so state removed
// const [dateProcessed, setDateProcessed] = useState<string>('')
const [currentIdx, setCurrentIdx] = useState(0)
const currentFace = faces[currentIdx]
@@ -28,7 +29,7 @@ export default function Identify() {
const [selectedSimilar, setSelectedSimilar] = useState<Record<number, boolean>>({})
const [uniqueFacesOnly, setUniqueFacesOnly] = useState(true)
// LocalStorage key for persisting settings
// SessionStorage key for persisting settings (clears when tab/window closes)
const SETTINGS_KEY = 'identify_settings'
const [people, setPeople] = useState<Person[]>([])
@@ -77,7 +78,8 @@ export default function Identify() {
min_quality: minQuality,
date_taken_from: dateFrom || undefined,
date_taken_to: dateTo || undefined,
date_processed: dateProcessed || undefined,
// date_processed filter is hidden, so don't send it
// date_processed: dateProcessed || undefined,
sort_by: sortBy,
sort_dir: sortDir,
tag_names: selectedTags.length > 0 ? selectedTags.join(', ') : undefined,
@@ -238,10 +240,10 @@ export default function Identify() {
}
}
// Load settings from localStorage on mount
// Load settings from sessionStorage on mount
useEffect(() => {
try {
const saved = localStorage.getItem(SETTINGS_KEY)
const saved = sessionStorage.getItem(SETTINGS_KEY)
if (saved) {
const settings = JSON.parse(saved)
if (settings.pageSize !== undefined) setPageSize(settings.pageSize)
@@ -250,20 +252,21 @@ export default function Identify() {
if (settings.sortDir !== undefined) setSortDir(settings.sortDir)
if (settings.dateFrom !== undefined) setDateFrom(settings.dateFrom)
if (settings.dateTo !== undefined) setDateTo(settings.dateTo)
if (settings.dateProcessed !== undefined) setDateProcessed(settings.dateProcessed)
// dateProcessed filter is hidden, so don't load it from localStorage
// if (settings.dateProcessed !== undefined) setDateProcessed(settings.dateProcessed)
if (settings.uniqueFacesOnly !== undefined) setUniqueFacesOnly(settings.uniqueFacesOnly)
if (settings.compareEnabled !== undefined) setCompareEnabled(settings.compareEnabled)
if (settings.selectedTags !== undefined) setSelectedTags(settings.selectedTags)
}
} catch (error) {
console.error('Error loading settings from localStorage:', error)
console.error('Error loading settings from sessionStorage:', error)
} finally {
setSettingsLoaded(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Save settings to localStorage whenever they change (but only after initial load)
// Save settings to sessionStorage whenever they change (but only after initial load)
useEffect(() => {
if (!settingsLoaded) return // Don't save during initial load
try {
@@ -274,16 +277,17 @@ export default function Identify() {
sortDir,
dateFrom,
dateTo,
dateProcessed,
// dateProcessed filter is hidden, so don't save it
// dateProcessed,
uniqueFacesOnly,
compareEnabled,
selectedTags,
}
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
sessionStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
} catch (error) {
console.error('Error saving settings to localStorage:', error)
console.error('Error saving settings to sessionStorage:', error)
}
}, [pageSize, minQuality, sortBy, sortDir, dateFrom, dateTo, dateProcessed, uniqueFacesOnly, compareEnabled, selectedTags, settingsLoaded])
}, [pageSize, minQuality, sortBy, sortDir, dateFrom, dateTo, uniqueFacesOnly, compareEnabled, selectedTags, settingsLoaded])
// Initial load on mount (after settings are loaded)
useEffect(() => {
@@ -551,11 +555,12 @@ export default function Identify() {
<option value="asc">Asc</option>
</select>
</div>
<div>
{/* Date Processed filter hidden for now */}
{/* <div>
<label className="block text-sm font-medium text-gray-700">Date Processed On</label>
<input type="date" value={dateProcessed} onChange={(e) => setDateProcessed(e.target.value)}
className="mt-1 block w-full border rounded px-2 py-1" />
</div>
</div> */}
<div>
<label className="block text-sm font-medium text-gray-700">Min Quality</label>
<input type="range" min={0} max={1} step={0.05} value={minQuality}
+5 -3
View File
@@ -3,7 +3,7 @@ import { photosApi, PhotoSearchResult } from '../api/photos'
import tagsApi, { TagResponse, PhotoTagItem } from '../api/tags'
import { apiClient } from '../api/client'
type SearchType = 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags'
type SearchType = 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed'
const SEARCH_TYPES: { value: SearchType; label: string }[] = [
{ value: 'name', label: 'Search photos by name' },
@@ -11,6 +11,8 @@ const SEARCH_TYPES: { value: SearchType; label: string }[] = [
{ value: 'tags', label: 'Search photos by tags' },
{ value: 'no_faces', label: 'Photos without faces' },
{ value: 'no_tags', label: 'Photos without tags' },
{ value: 'processed', label: 'Search processed photos' },
{ value: 'unprocessed', label: 'Search un-processed photos' },
]
type SortColumn = 'person' | 'tags' | 'processed' | 'path' | 'date_taken'
@@ -124,7 +126,7 @@ export default function Search() {
}
useEffect(() => {
if (searchType === 'no_faces' || searchType === 'no_tags') {
if (searchType === 'no_faces' || searchType === 'no_tags' || searchType === 'processed' || searchType === 'unprocessed') {
handleSearch()
}
// Clear selected tags when switching away from tag search
@@ -461,7 +463,7 @@ export default function Search() {
</div>
{/* Search Inputs */}
{(searchType !== 'no_faces' && searchType !== 'no_tags') && (
{(searchType !== 'no_faces' && searchType !== 'no_tags' && searchType !== 'processed' && searchType !== 'unprocessed') && (
<div className="bg-white rounded-lg shadow py-2 px-4 mb-2">
{searchType === 'name' && (
<div className="flex items-center gap-2">