From aba061dd435216c1f1953432e6b728db6ad7eb59 Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 10 Apr 2026 10:51:22 +0200 Subject: [PATCH] fix: People view shows person's photos inline instead of navigating away Clicking a person card now opens a detail sub-view within the People section showing their photo grid. Back arrow returns to the card grid. Photos are clickable to open the preview. Rename is available in both the card grid and the detail header. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/people/PeopleView.tsx | 192 +++++++++++++++--- 1 file changed, 167 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/people/PeopleView.tsx b/frontend/src/components/people/PeopleView.tsx index 42e2432..ac7277f 100644 --- a/frontend/src/components/people/PeopleView.tsx +++ b/frontend/src/components/people/PeopleView.tsx @@ -1,23 +1,28 @@ import { useState } from 'react' -import { Users, Pencil, Check, X, Loader2 } from 'lucide-react' +import { Users, Pencil, Check, X, Loader2, ArrowLeft } from 'lucide-react' import clsx from 'clsx' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTagsQuery } from '../../hooks/useTagsQuery' -import { tags as tagsApi, photos as photosApi, type Tag } from '../../services/api' -import { useFilterStore } from '../../store/filterStore' +import { + tags as tagsApi, + photos as photosApi, + search as searchApi, + type Tag, +} from '../../services/api' +import { usePhotoStore } from '../../store/photoStore' import { toast } from '../ToastContainer' /** - * People view — grid of face cluster cards. Each card shows the - * representative photo thumbnail, the cluster name, and a photo count. - * Clicking a card navigates to the all-photos section filtered to that - * person's tag. The name is editable inline. + * People view — two states: + * 1. Grid of face cluster cards (default) + * 2. Detail view showing a person's photos when a card is clicked */ export function PeopleView() { const { data: clusters = [], isLoading } = useTagsQuery('face_cluster') - const navigateToSection = useFilterStore((s) => s.navigateToSection) const queryClient = useQueryClient() + const openPreview = usePhotoStore((s) => s.openPreview) + const [selectedPerson, setSelectedPerson] = useState(null) const [editingId, setEditingId] = useState(null) const [editName, setEditName] = useState('') @@ -27,6 +32,10 @@ export function PeopleView() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tags'] }) setEditingId(null) + // Update the selected person's name if we're renaming the active one + if (selectedPerson && editingId === selectedPerson.id) { + setSelectedPerson({ ...selectedPerson, name: editName.trim() }) + } toast.success('Renamed') }, onError: (e: any) => @@ -43,11 +52,24 @@ export function PeopleView() { renameMutation.mutate({ id: editingId, name: editName.trim() }) } - const handleCardClick = (tag: Tag) => { - if (editingId === tag.id) return - navigateToSection('all-photos', { tagIds: [tag.id] }) + // ── Detail view: a person's photos ───────────────────────────────── + if (selectedPerson) { + return ( + setSelectedPerson(null)} + onRename={() => startEditing(selectedPerson)} + editingId={editingId} + editName={editName} + setEditName={setEditName} + submitRename={submitRename} + cancelEdit={() => setEditingId(null)} + openPreview={openPreview} + /> + ) } + // ── Card grid ────────────────────────────────────────────────────── if (isLoading) { return (
@@ -87,9 +109,10 @@ export function PeopleView() { 'group cursor-pointer overflow-hidden rounded-lg border border-border bg-surface transition-all hover:border-primary/50 hover:shadow-md', editingId === tag.id && 'border-primary ring-1 ring-primary/30' )} - onClick={() => handleCardClick(tag)} + onClick={() => { + if (editingId !== tag.id) setSelectedPerson(tag) + }} > - {/* Thumbnail — representative photo or placeholder */}
{tag.representative_photo_id ? ( )} - {/* Photo count badge */} {tag.photo_count} - {/* Edit button — visible on hover */}
- {/* Name — inline editable */}
{editingId === tag.id ? (
e.stopPropagation()}> @@ -135,16 +155,10 @@ export function PeopleView() { }} className="min-w-0 flex-1 rounded border border-border bg-bg px-1.5 py-0.5 text-xs text-text focus:border-primary focus:outline-none" /> - -
@@ -158,3 +172,131 @@ export function PeopleView() {
) } + + +// ── Person detail sub-view ─────────────────────────────────────────── + +interface PersonDetailProps { + person: Tag + onBack: () => void + onRename: () => void + editingId: string | null + editName: string + setEditName: (v: string) => void + submitRename: () => void + cancelEdit: () => void + openPreview: (photoId: string, photoIds: string[]) => void +} + +function PersonDetail({ + person, + onBack, + onRename, + editingId, + editName, + setEditName, + submitRename, + cancelEdit, + openPreview, +}: PersonDetailProps) { + const { data, isLoading } = useQuery({ + queryKey: ['person-photos', person.id], + queryFn: async () => { + const resp = await searchApi.query({ + filters: { tag_ids: [person.id] }, + limit: 500, + }) + return resp.results + }, + }) + + const photos = data ?? [] + const isEditing = editingId === person.id + + return ( +
+ {/* Header */} +
+ + + {isEditing ? ( +
+ setEditName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') submitRename() + if (e.key === 'Escape') cancelEdit() + }} + className="rounded border border-border bg-bg px-2 py-1 text-sm text-text focus:border-primary focus:outline-none" + /> + + +
+ ) : ( +
+

{person.name}

+ +
+ )} + + + {photos.length} {photos.length === 1 ? 'photo' : 'photos'} + +
+ + {/* Photo grid */} +
+ {isLoading ? ( +
+ + Loading photos... +
+ ) : photos.length === 0 ? ( +

No photos found

+ ) : ( +
+ {photos.map((photo) => ( +
+ openPreview( + photo.id, + photos.map((p) => p.id) + ) + } + > +
+ {photo.filename} +
+
+ ))} +
+ )} +
+
+ ) +}