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} +
+
+ ))} +
+ )} +
+
+ ) +}