feat: Enhance UI with emoji page titles and improve tagging functionality

This commit updates the Layout component to include emojis in page titles for better visual cues. The Login component removes default credential placeholders for improved security. Additionally, the Search component is enhanced to allow immediate tagging of selected photos with existing tags, and it supports adding multiple tags at once, improving user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09 2025-12-09 13:01:35 -05:00
parent 0a109b198a
commit 6e196ff859
4 changed files with 67 additions and 32 deletions

View File

@ -77,7 +77,7 @@ export default function Layout() {
// Get page title based on route
const getPageTitle = () => {
const route = location.pathname
if (route === '/') return 'Home Page'
if (route === '/') return '🏠 Home Page'
if (route === '/scan') return '🗂️ Scan Photos'
if (route === '/process') return '⚙️ Process Faces'
if (route === '/search') return '🔍 Search Photos'

View File

@ -87,7 +87,6 @@ export default function Login() {
onChange={(e) => setUsername(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="admin"
/>
</div>
@ -106,7 +105,6 @@ export default function Login() {
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="admin"
/>
<button
type="button"
@ -127,10 +125,6 @@ export default function Login() {
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<div className="mt-6 text-center text-sm text-gray-500">
<p>Default credentials: admin / admin</p>
</div>
</div>
</div>
</div>

View File

@ -519,37 +519,76 @@ export default function Search() {
return commonTags
}, [photoTags, selectedPhotos])
const handleSelectExistingTag = async (tagName: string) => {
if (selectedPhotos.size === 0 || !tagName.trim()) {
return
}
try {
// Add the selected tag to photos
await tagsApi.addToPhotos({
photo_ids: Array.from(selectedPhotos),
tag_names: [tagName.trim()],
})
// Clear the dropdown selection
setSelectedTagName('')
// Reload tags for selected photos so the new tag appears in the Tags area
await loadPhotoTags()
} catch (error) {
console.error('Error tagging photos:', error)
alert('Error tagging photos. Please try again.')
}
}
const handleAddTag = async () => {
if (selectedPhotos.size === 0) {
alert('Please select photos to tag.')
return
}
// Determine which tag to use: new tag input or selected from dropdown
const tagToAdd = newTagName.trim() || selectedTagName.trim()
// Collect both tags: selected existing tag and new tag name
const tagsToAdd: string[] = []
if (!tagToAdd) {
if (selectedTagName.trim()) {
tagsToAdd.push(selectedTagName.trim())
}
if (newTagName.trim()) {
tagsToAdd.push(newTagName.trim())
}
if (tagsToAdd.length === 0) {
alert('Please select a tag or enter a new tag name.')
return
}
try {
// If it's a new tag (from input field), create it first
if (newTagName.trim()) {
await tagsApi.create(newTagName.trim())
// Refresh available tags list
await loadTags()
// Clear the new tag input
setNewTagName('')
// Create any new tags first
const newTags = tagsToAdd.filter(tag =>
!availableTags.some(availableTag =>
availableTag.tag_name.toLowerCase() === tag.toLowerCase()
)
)
for (const newTag of newTags) {
await tagsApi.create(newTag)
}
// Add tag to photos
// Refresh available tags list if we created any
if (newTags.length > 0) {
await loadTags()
}
// Add all tags to photos in a single API call
await tagsApi.addToPhotos({
photo_ids: Array.from(selectedPhotos),
tag_names: [tagToAdd],
tag_names: tagsToAdd,
})
// Clear inputs after successful tagging
setSelectedTagName('')
setNewTagName('')
// Reload tags for selected photos
await loadPhotoTags()
} catch (error) {
@ -1480,10 +1519,10 @@ export default function Search() {
<select
value={selectedTagName}
onChange={(e) => {
setSelectedTagName(e.target.value)
// Clear new tag input when selecting from dropdown
if (e.target.value) {
setNewTagName('')
const tagName = e.target.value
if (tagName) {
// Immediately add the tag and clear the dropdown
handleSelectExistingTag(tagName)
}
}}
className="w-full px-3 py-2 border border-gray-300 rounded"
@ -1495,20 +1534,19 @@ export default function Search() {
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Selecting a tag will immediately add it to the photos. You can select multiple tags in a row.
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Or Enter New Tag Name:
Enter New Tag Name:
</label>
<input
type="text"
value={newTagName}
onChange={(e) => {
setNewTagName(e.target.value)
// Clear selected tag when typing new tag
if (e.target.value) {
setSelectedTagName('')
}
}}
className="w-full px-3 py-2 border border-gray-300 rounded"
placeholder="Type new tag name..."
@ -1519,7 +1557,7 @@ export default function Search() {
}}
/>
<p className="text-xs text-gray-500 mt-1">
New tags will be created in the database automatically.
You can select an existing tag and enter a new tag name to add both at once. New tags will be created in the database automatically.
</p>
</div>

View File

@ -229,6 +229,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
# Query 1: Get all photos with face counts using LEFT JOIN and GROUP BY
# This gets face_count and unidentified_face_count in one query
# Note: Excludes excluded faces (Face.excluded == False) to match identify UI behavior
photos_with_counts = (
db.query(
Photo.id,
@ -238,14 +239,14 @@ def get_photos_with_tags(db: Session) -> List[dict]:
Photo.date_taken,
Photo.date_added,
Photo.media_type,
# Face count (all faces)
# Face count (non-excluded faces only)
func.count(distinct(Face.id)).label('face_count'),
# Unidentified face count (faces with person_id IS NULL)
# Unidentified face count (non-excluded faces with person_id IS NULL)
func.sum(
case((Face.person_id.is_(None), 1), else_=0)
).label('unidentified_face_count'),
)
.outerjoin(Face, Photo.id == Face.photo_id)
.outerjoin(Face, (Photo.id == Face.photo_id) & (Face.excluded == False))
.group_by(
Photo.id,
Photo.filename,
@ -292,6 +293,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
# Query 3: Get all people for all photos in one query
# Get distinct people per photo, then format names in Python
# Note: Excludes excluded faces to match face count behavior
people_data = (
db.query(
Face.photo_id,
@ -304,6 +306,7 @@ def get_photos_with_tags(db: Session) -> List[dict]:
.join(Person, Face.person_id == Person.id)
.filter(Face.photo_id.in_(photo_ids))
.filter(Face.person_id.isnot(None))
.filter(Face.excluded == False)
.distinct()
.order_by(Face.photo_id, Person.last_name, Person.first_name)
.all()