feat: Enhance Layout and Search components with dynamic page titles and media type filtering

This commit introduces a new function in the Layout component to dynamically set page titles based on the current route, improving user navigation. Additionally, the Search component has been updated to include a media type filter, allowing users to filter results by images or videos. The UI has been enhanced with collapsible filters for better organization. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-12-02 13:30:51 -05:00
parent 9c6a2ff05e
commit c6055737fb
19 changed files with 225 additions and 74 deletions
+48 -18
View File
@@ -74,26 +74,56 @@ export default function Layout() {
const visibleMaintenance = filterNavItems(maintenanceNavItems)
const visibleFooter = filterNavItems(footerNavItems)
// Get page title based on route
const getPageTitle = () => {
const route = location.pathname
if (route === '/') return 'Dashboard'
if (route === '/scan') return '🗂️ Scan Photos'
if (route === '/process') return '⚙️ Process Faces'
if (route === '/search') return '🔍 Search Photos'
if (route === '/identify') return '👤 Identify'
if (route === '/auto-match') return '🤖 Auto-Match Faces'
if (route === '/modify') return '✏️ Modify Identified'
if (route === '/tags') return '🏷️ Photos tagging interface'
if (route === '/manage-photos') return 'Manage Photos'
if (route === '/faces-maintenance') return '🔧 Faces Maintenance'
if (route === '/approve-identified') return '✅ Approve Identified'
if (route === '/manage-users') return '👥 Manage Users'
if (route === '/reported-photos') return '🚩 Reported Photos'
if (route === '/pending-linkages') return '🔖 User Tagged Photos'
if (route === '/pending-photos') return '📤 Manage User Uploaded Photos'
if (route === '/settings') return 'Settings'
if (route === '/help') return '📚 Help'
return 'PunimTag'
}
return (
<div className="min-h-screen bg-gray-50">
{/* Top bar */}
<div className="bg-white border-b border-gray-200 shadow-sm">
<div className="max-w-7xl mx-auto px-2 sm:px-4 lg:px-6">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<span className="text-2xl">🏠</span>
<h1 className="text-xl font-bold text-gray-900">PunimTag</h1>
</Link>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{username}</span>
<button
onClick={logout}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900"
>
Logout
</button>
<div className="flex">
{/* Left sidebar - fixed position */}
<div className="fixed left-0 top-0 w-64 bg-white border-r border-gray-200 h-12 flex items-center px-4 z-10">
<Link to="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<span className="text-2xl">🏠</span>
<h2 className="text-xl font-bold text-gray-900">PunimTag</h2>
</Link>
</div>
{/* Header content - aligned with main content */}
<div className="ml-64 flex-1 px-4">
<div className="flex justify-between items-center h-12">
<div className="flex items-center">
<h1 className="text-lg font-bold text-gray-900">{getPageTitle()}</h1>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{username}</span>
<button
onClick={logout}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900"
>
Logout
</button>
</div>
</div>
</div>
</div>
@@ -101,7 +131,7 @@ export default function Layout() {
<div className="flex relative">
{/* Left sidebar - fixed position */}
<div className="fixed left-0 top-16 w-64 bg-white border-r border-gray-200 h-[calc(100vh-4rem)] overflow-y-auto">
<div className="fixed left-0 top-12 w-64 bg-white border-r border-gray-200 h-[calc(100vh-3rem)] overflow-y-auto">
<nav className="p-4 space-y-1">
{visiblePrimary.map((item) => renderNavLink(item))}
@@ -132,7 +162,7 @@ export default function Layout() {
</div>
{/* Main content - with left margin to account for fixed sidebar */}
<div className="flex-1 ml-64 p-6">
<div className="flex-1 ml-64 p-4">
<Outlet />
</div>
</div>
-1
View File
@@ -207,7 +207,6 @@ export default function ApproveIdentified() {
return (
<div>
<div className="bg-white rounded-lg shadow p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Approve Identified</h1>
{loading && (
<div className="text-center py-8">
-1
View File
@@ -525,7 +525,6 @@ export default function AutoMatch() {
return (
<div className="flex flex-col h-full">
<h1 className="text-2xl font-bold text-gray-900 mb-4">🔗 Auto-Match Faces</h1>
{/* Configuration */}
<div className="bg-white rounded-lg shadow p-4 mb-4">
-1
View File
@@ -129,7 +129,6 @@ export default function FacesMaintenance() {
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Faces Maintenance</h1>
{/* Controls */}
<div className="bg-white rounded-lg shadow mb-4 p-4">
-1
View File
@@ -32,7 +32,6 @@ export default function Help() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">📚 Help</h1>
{renderPageContent()}
</div>
)
+71 -27
View File
@@ -73,6 +73,9 @@ export default function Identify() {
const [statsDateFrom, setStatsDateFrom] = useState<string>('')
const [statsDateTo, setStatsDateTo] = useState<string>('')
// Tab state
const [activeTab, setActiveTab] = useState<'faces' | 'videos'>('faces')
// Store form data per face ID (matching desktop behavior)
const [faceFormData, setFaceFormData] = useState<Record<number, {
personId?: number
@@ -693,27 +696,55 @@ export default function Identify() {
return (
<div>
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold text-gray-900">
Identify
{photoIds && (
<span className="ml-2 text-sm font-normal text-gray-600">
(Filtered by {photoIds.length} photo{photoIds.length !== 1 ? 's' : ''})
</span>
{photoIds && (
<div className="mb-4">
<span className="text-sm font-normal text-gray-600">
(Filtered by {photoIds.length} photo{photoIds.length !== 1 ? 's' : ''})
</span>
</div>
)}
{/* Tabs */}
<div className="mb-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<nav className="flex space-x-8" aria-label="Tabs">
<button
onClick={() => setActiveTab('faces')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'faces'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Identify Faces
</button>
<button
onClick={() => setActiveTab('videos')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'videos'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Identify People in Videos
</button>
</nav>
{isAdmin && activeTab === 'faces' && (
<button
type="button"
onClick={handleOpenStats}
className="px-3 py-1.5 text-sm bg-green-100 text-green-700 rounded-md hover:bg-green-200 font-medium"
>
📊 Statistics
</button>
)}
</h1>
{isAdmin && (
<button
type="button"
onClick={handleOpenStats}
className="px-3 py-1.5 text-sm bg-green-100 text-green-700 rounded-md hover:bg-green-200 font-medium"
>
📊 Statistics
</button>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4">
{/* Tab Content */}
{activeTab === 'faces' && (
<>
<div className="grid grid-cols-12 gap-4">
{/* Left: Controls and current face */}
<div className="col-span-4">
{/* Unique Faces Checkbox and Batch Size - Outside Filters */}
@@ -1043,12 +1074,7 @@ export default function Identify() {
<div className="col-span-8">
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<input id="compare" type="checkbox" checked={compareEnabled}
onChange={(e) => setCompareEnabled(e.target.checked)} />
<label htmlFor="compare" className="text-sm text-gray-700">Compare with similar faces</label>
</div>
<div className="space-x-2">
<div className="flex space-x-2">
<button className="px-2 py-1 text-sm border rounded"
onClick={() => setSelectedSimilar(Object.fromEntries(similar.map(s => [s.id, true])))}
disabled={!compareEnabled || similar.length === 0}>Select All</button>
@@ -1056,6 +1082,11 @@ export default function Identify() {
onClick={() => setSelectedSimilar({})}
disabled={!compareEnabled || similar.length === 0}>Clear All</button>
</div>
<div className="flex items-center gap-2">
<input id="compare" type="checkbox" checked={compareEnabled}
onChange={(e) => setCompareEnabled(e.target.checked)} />
<label htmlFor="compare" className="text-sm text-gray-700">Compare with similar faces</label>
</div>
</div>
{!compareEnabled ? (
<div className="text-gray-500 py-4 text-center">Enable 'Compare similar faces' to see similar faces</div>
@@ -1155,8 +1186,8 @@ export default function Identify() {
</div>
</div>
{/* Identification statistics modal */}
{showStats && (
{/* Identification statistics modal */}
{showStats && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
@@ -1293,6 +1324,19 @@ export default function Identify() {
</div>
</div>
</div>
)}
</>
)}
{activeTab === 'videos' && (
<div className="bg-white rounded-lg shadow p-8 text-center">
<h2 className="text-xl font-semibold text-gray-700 mb-2">
Identify People in Videos
</h2>
<p className="text-gray-500">
This functionality will be available in a future update.
</p>
</div>
)}
</div>
)
-1
View File
@@ -1,7 +1,6 @@
export default function ManagePhotos() {
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Manage Photos</h1>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-gray-600">Photo management functionality coming soon...</p>
+1 -2
View File
@@ -760,8 +760,7 @@ const getDisplayRoleLabel = (user: UserResponse): string => {
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Manage Users</h1>
<div className="flex justify-end items-center mb-6">
{activeTab !== 'roles' && (
<button
onClick={() => {
-1
View File
@@ -437,7 +437,6 @@ export default function Modify() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">✏️ Modify Identified</h1>
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded text-red-700">
-1
View File
@@ -452,7 +452,6 @@ export default function PendingPhotos() {
)}
<div className="bg-white rounded-lg shadow p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Manage User Uploaded Photos</h1>
{loading && (
<div className="text-center py-8">
-1
View File
@@ -221,7 +221,6 @@ export default function Process() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Process Faces</h1>
<div className="space-y-6">
{/* Configuration Section */}
-1
View File
@@ -211,7 +211,6 @@ export default function ReportedPhotos() {
return (
<div>
<div className="bg-white rounded-lg shadow p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Reported Photos</h1>
{loading && (
<div className="text-center py-8">
-1
View File
@@ -223,7 +223,6 @@ export default function Scan() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Scan Photos</h1>
<div className="space-y-6">
{/* Folder Scan Section */}
+44 -4
View File
@@ -27,6 +27,7 @@ export default function Search() {
const canDeletePhotos = hasPermission('faces_maintenance')
const [searchType, setSearchType] = useState<SearchType>('name')
const [tagsExpanded, setTagsExpanded] = useState(true) // Default to expanded
const [filtersExpanded, setFiltersExpanded] = useState(false) // Default to collapsed
// Search inputs
const [personName, setPersonName] = useState('')
@@ -34,6 +35,7 @@ export default function Search() {
const [matchAll, setMatchAll] = useState(false)
const [dateFrom, setDateFrom] = useState('')
const [dateTo, setDateTo] = useState('')
const [mediaType, setMediaType] = useState<string>('all') // Default to 'all'
// Results
const [results, setResults] = useState<PhotoSearchResult[]>([])
@@ -91,6 +93,11 @@ export default function Search() {
page_size: pageSize,
}
// Add media type filter if not 'all'
if (mediaType && mediaType !== 'all') {
params.media_type = mediaType
}
if (searchType === 'name') {
if (!personName.trim()) {
alert('Please enter at least one name to search.')
@@ -520,6 +527,11 @@ export default function Search() {
page_size: maxPageSize,
}
// Add media type filter if not 'all'
if (mediaType && mediaType !== 'all') {
baseParams.media_type = mediaType
}
if (searchType === 'name') {
baseParams.person_name = personName.trim()
} else if (searchType === 'date') {
@@ -572,8 +584,6 @@ export default function Search() {
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">🔎 Search Photos</h1>
{/* Search Type Selector */}
<div className="bg-white rounded-lg shadow py-2 px-4 mb-2">
<div className="flex items-center gap-2">
@@ -592,7 +602,37 @@ export default function Search() {
</div>
</div>
{/* Filters removed: folder location filter is no longer supported */}
{/* Filters */}
<div className="bg-white rounded-lg shadow py-2 px-4 mb-2">
<div className="flex items-center gap-2">
<h2 className="text-sm font-medium text-gray-700">Filters</h2>
<button
onClick={() => setFiltersExpanded(!filtersExpanded)}
className="text-lg text-gray-600 hover:text-gray-800"
title={filtersExpanded ? 'Collapse' : 'Expand'}
>
{filtersExpanded ? '▼' : '▶'}
</button>
</div>
{filtersExpanded && (
<div className="mt-3 space-y-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Media Type:
</label>
<select
value={mediaType}
onChange={(e) => setMediaType(e.target.value)}
className="w-full border rounded px-3 py-2"
>
<option value="all">All</option>
<option value="image">Photos</option>
<option value="video">Videos</option>
</select>
</div>
</div>
)}
</div>
{/* Search Inputs */}
{(searchType !== 'no_faces' && searchType !== 'no_tags' && searchType !== 'processed' && searchType !== 'unprocessed') && (
@@ -782,7 +822,7 @@ export default function Search() {
disabled={selectedPhotos.size === 0}
className="px-3 py-1 text-sm border rounded hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400"
>
Clear all selected
Unselect All
</button>
</div>
</div>
-1
View File
@@ -5,7 +5,6 @@ export default function Settings() {
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Settings</h1>
<div className="bg-white rounded-lg shadow p-6 mb-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Developer Options</h2>
+1 -2
View File
@@ -393,8 +393,7 @@ export default function Tags() {
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold text-gray-900">Photos tagging interface</h1>
<div className="flex items-center justify-end mb-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">View:</label>
-1
View File
@@ -294,7 +294,6 @@ export default function UserTaggedPhotos() {
<div className="space-y-6">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">User Tagged Photos</h1>
<p className="text-gray-600">
Review tags suggested by users. Approving creates/links the tag to the selected photo.
</p>