diff --git a/client/src/App.jsx b/client/src/App.jsx
index e2c6f32..41bc190 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
-import { fetchPhotos, fetchLibraryStats, fetchVideoStats } from './api/photos.js';
+import { fetchPhotos, fetchLibraryStats, fetchVideoStats, bulkStar, bulkAddToCollection } from './api/photos.js';
import Timeline from './components/Timeline.jsx';
import Lightbox from './components/Lightbox.jsx';
import FilterSidebar from './components/FilterSidebar.jsx';
@@ -11,6 +11,7 @@ import { lazy, Suspense } from 'react';
const MapView = lazy(() => import('./components/MapView.jsx'));
const MemoriesView = lazy(() => import('./components/MemoriesView.jsx'));
const VideosView = lazy(() => import('./components/VideosView.jsx'));
+const AlbumsView = lazy(() => import('./components/AlbumsView.jsx'));
const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
const EMPTY_VIDEO_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
@@ -30,9 +31,10 @@ export default function App() {
const [videoFilters, setVideoFilters] = useState(EMPTY_VIDEO_FILTERS);
const [videoStats, setVideoStats] = useState(null);
- const [lbPhotos, setLbPhotos] = useState(null);
- const [lbIndex, setLbIndex] = useState(null);
+ const [lbPhotos, setLbPhotos] = useState(null);
+ const [lbIndex, setLbIndex] = useState(null);
const [showSettings, setShowSettings] = useState(false);
+ const [selected, setSelected] = useState(new Set());
const loadingRef = useRef(false);
const sentinelRef = useRef(null);
@@ -90,6 +92,46 @@ export default function App() {
return () => obs.disconnect();
}, [nextCursor, view, filters]);
+ function toggleSelect(photoId) {
+ setSelected(prev => {
+ const next = new Set(prev);
+ next.has(photoId) ? next.delete(photoId) : next.add(photoId);
+ return next;
+ });
+ }
+
+ function clearSelection() { setSelected(new Set()); }
+
+ async function handleBulkStar(starred) {
+ const ids = [...selected];
+ try {
+ await bulkStar(ids, starred);
+ const idSet = new Set(ids);
+ setPhotos(prev => prev.map(p => idSet.has(p.id) ? { ...p, starred } : p));
+ clearSelection();
+ loadStats();
+ } catch {}
+ }
+
+ async function handleBulkAddToCollection(collectionId) {
+ if (!collectionId) return;
+ try {
+ await bulkAddToCollection([...selected], Number(collectionId));
+ clearSelection();
+ } catch {}
+ }
+
+ function handleSelectAlbum({ type, collectionId, year, city, camera }) {
+ const f = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
+ if (type === 'collection') f.collectionId = String(collectionId);
+ else if (type === 'year') { f.from = `${year}-01-01`; f.to = `${year}-12-31`; }
+ else if (type === 'city') f.city = city;
+ else if (type === 'camera') f.q = camera;
+ setFilters(f);
+ setView('library');
+ clearSelection();
+ }
+
function openLightbox(photosArr, idx) {
setLbPhotos(photosArr);
setLbIndex(idx);
@@ -128,7 +170,7 @@ export default function App() {
}>
+
+
+ )}
+
{/* Videos */}
{view === 'videos' && (
Loading…}>
@@ -271,6 +327,41 @@ export default function App() {
+ {/* Bulk action bar */}
+ {selected.size > 0 && view === 'library' && (
+
+
+ {selected.size} selected
+
+
+
+
+ {stats?.collections?.length > 0 && (
+
+ )}
+
+
+
+ )}
+
{/* Settings panel */}
{showSettings && setShowSettings(false)} />}
@@ -288,6 +379,11 @@ export default function App() {
);
}
+const bulkBtn = {
+ background: 'transparent', border: '1px solid #333', borderRadius: 6,
+ color: '#ccc', cursor: 'pointer', fontSize: 12, padding: '5px 11px',
+};
+
function Tab({ active, onClick, children }) {
return (