diff --git a/frontend/src/App.css b/frontend/src/App.css
index 0cbead653..95dbcd78c 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -226,7 +226,7 @@
}
.thin-scrollbar::-webkit-scrollbar-thumb:hover {
- background-color: var(--muted-foreground);
+ background-color: var(--muted-foreground);
}
.no-select {
diff --git a/frontend/src/components/FaceCollections.tsx b/frontend/src/components/FaceCollections.tsx
index 582348204..58e257f52 100644
--- a/frontend/src/components/FaceCollections.tsx
+++ b/frontend/src/components/FaceCollections.tsx
@@ -1,11 +1,11 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router';
import { Card, CardContent } from '@/components/ui/card';
import { PersonAvatar } from '@/components/PersonAvatar';
import { getPersonName, getPhotoCountText } from '@/utils/personUtils';
import { Button } from '@/components/ui/button';
-import { Users } from 'lucide-react';
+import { ChevronLeft, ChevronRight, Users } from 'lucide-react';
import { MultiPersonSearchDialog } from '@/components/Dialog/MultiPersonSearchDialog';
import { RootState } from '@/app/store';
import { setClusters } from '@/features/faceClustersSlice';
@@ -20,10 +20,15 @@ interface FaceCollectionsProps {
) => void;
}
+// One row at xl:grid-cols-8. Keeps the card height fixed on every page,
+// not just the default view.
+const PAGE_SIZE = 8;
+
export function FaceCollections({ onSearchActivated }: FaceCollectionsProps) {
const navigate = useNavigate();
const dispatch = useDispatch();
const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false);
+ const [page, setPage] = useState(0);
const { clusters } = useSelector((state: RootState) => state.faceClusters);
@@ -39,6 +44,24 @@ export function FaceCollections({ onSearchActivated }: FaceCollectionsProps) {
}
}, [clustersData, clustersSuccess, dispatch]);
+ // Highest face_count first, so the most prominent people show up on page 1.
+ const sortedClusters = useMemo(
+ () =>
+ [...(clusters ?? [])].sort(
+ (a: Cluster, b: Cluster) => (b.face_count ?? 0) - (a.face_count ?? 0),
+ ),
+ [clusters],
+ );
+
+ // Clamp page in case the cluster list shrinks (e.g. after a delete) while
+ // the user is on a later page.
+ const totalPages = Math.max(1, Math.ceil(sortedClusters.length / PAGE_SIZE));
+ const currentPage = Math.min(page, totalPages - 1);
+
+ useEffect(() => {
+ setPage((previousPage) => Math.min(previousPage, totalPages - 1));
+ }, [totalPages]);
+
const handlePersonClick = (clusterId: string) => {
navigate(`/person/${clusterId}`);
};
@@ -57,6 +80,12 @@ export function FaceCollections({ onSearchActivated }: FaceCollectionsProps) {
);
}
+ const hasMultiplePages = totalPages > 1;
+ const visibleClusters = sortedClusters.slice(
+ currentPage * PAGE_SIZE,
+ (currentPage + 1) * PAGE_SIZE,
+ );
+
return (
+
+