diff --git a/client/src/App.jsx b/client/src/App.jsx index 6aeda4b..e2c6f32 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 } from './api/photos.js'; +import { fetchPhotos, fetchLibraryStats, fetchVideoStats } from './api/photos.js'; import Timeline from './components/Timeline.jsx'; import Lightbox from './components/Lightbox.jsx'; import FilterSidebar from './components/FilterSidebar.jsx'; @@ -12,7 +12,8 @@ const MapView = lazy(() => import('./components/MapView.jsx')); const MemoriesView = lazy(() => import('./components/MemoriesView.jsx')); const VideosView = lazy(() => import('./components/VideosView.jsx')); -const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' }; +const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' }; +const EMPTY_VIDEO_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' }; const SIDEBAR_W = 210; export default function App() { @@ -24,8 +25,10 @@ export default function App() { const [error, setError] = useState(null); const [dupeCount, setDupeCount] = useState(0); - const [filters, setFilters] = useState(EMPTY_FILTERS); - const [stats, setStats] = useState(null); + const [filters, setFilters] = useState(EMPTY_FILTERS); + const [stats, setStats] = useState(null); + const [videoFilters, setVideoFilters] = useState(EMPTY_VIDEO_FILTERS); + const [videoStats, setVideoStats] = useState(null); const [lbPhotos, setLbPhotos] = useState(null); const [lbIndex, setLbIndex] = useState(null); @@ -40,6 +43,12 @@ export default function App() { useEffect(() => { loadStats(); }, []); + useEffect(() => { + if (view === 'videos') { + fetchVideoStats().then(setVideoStats).catch(() => {}); + } + }, [view]); + // Reset + reload when filters change (debounce text search) useEffect(() => { const delay = filters.q ? 300 : 0; @@ -167,20 +176,31 @@ export default function App() { {/* ── Body (sidebar + content) ── */}
- {/* Sidebar — only in library view */} - {view === 'library' && ( + {/* Sidebar — library and videos views */} + {(view === 'library' || view === 'videos') && ( )} @@ -221,7 +241,7 @@ export default function App() { {/* Videos */} {view === 'videos' && ( Loading…

}> - +
)} diff --git a/client/src/api/photos.js b/client/src/api/photos.js index c8a4cb8..0cbbf13 100644 --- a/client/src/api/photos.js +++ b/client/src/api/photos.js @@ -110,8 +110,17 @@ export function triggerSync(source) { } // Videos -export function fetchVideos() { - return api('/api/videos'); +export function fetchVideoStats() { + return api('/api/videos/stats'); +} + +export function fetchVideos({ from, to, city } = {}) { + const p = new URLSearchParams(); + if (from) p.set('from', from); + if (to) p.set('to', to); + if (city) p.set('city', city); + const qs = p.toString(); + return api(`/api/videos${qs ? '?' + qs : ''}`); } // Memories diff --git a/client/src/components/FilterSidebar.jsx b/client/src/components/FilterSidebar.jsx index a75beaa..fa31422 100644 --- a/client/src/components/FilterSidebar.jsx +++ b/client/src/components/FilterSidebar.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; const MAX_DEFAULT = 5; -export default function FilterSidebar({ filters, onChange, stats, total }) { +export default function FilterSidebar({ filters, onChange, stats, total, allLabel = 'All Photos' }) { const [expanded, setExpanded] = useState({}); function toggle(section) { @@ -55,7 +55,7 @@ export default function FilterSidebar({ filters, onChange, stats, total }) { active={isAll && !filters.q} onClick={selectAll} icon="⊞" - label="All Photos" + label={allLabel} count={stats?.total ?? total} /> diff --git a/client/src/components/VideosView.jsx b/client/src/components/VideosView.jsx index 4a736de..feda2a5 100644 --- a/client/src/components/VideosView.jsx +++ b/client/src/components/VideosView.jsx @@ -44,16 +44,18 @@ function fmtSize(b) { // ── Component ───────────────────────────────────────────────────────────────── -export default function VideosView() { +export default function VideosView({ filters = {} }) { const [videos, setVideos] = useState(null); const [playing, setPlaying] = useState(null); const [error, setError] = useState(null); useEffect(() => { - fetchVideos() + setVideos(null); + setError(null); + fetchVideos({ from: filters.from, to: filters.to, city: filters.city }) .then(d => setVideos(d.videos)) .catch(e => setError(e.message)); - }, []); + }, [filters.from, filters.to, filters.city]); useEffect(() => { if (!playing) return; diff --git a/server/index.js b/server/index.js index 90946c3..11d02e5 100644 --- a/server/index.js +++ b/server/index.js @@ -515,12 +515,28 @@ app.post('/api/admin/clear-dismissals', (_req, res) => { // ── Videos ──────────────────────────────────────────────────────────────────── -app.get('/api/videos', (_req, res) => { - const rows = db.raw.prepare(` - SELECT ${SELECT_COLS} FROM photos p - WHERE p.media_type = 'video' - ORDER BY p.date_taken DESC, p.id DESC - `).all(); +app.get('/api/videos/stats', (_req, res) => { + const total = db.raw.prepare("SELECT COUNT(*) AS n FROM photos WHERE media_type = 'video'").get().n; + const years = db.raw.prepare( + "SELECT strftime('%Y', date_taken) AS year, COUNT(*) AS count FROM photos WHERE media_type = 'video' AND date_taken IS NOT NULL GROUP BY year ORDER BY year DESC" + ).all(); + const cities = db.raw.prepare( + "SELECT place_city AS city, place_country AS country, COUNT(*) AS count FROM photos WHERE media_type = 'video' AND place_city IS NOT NULL GROUP BY place_city ORDER BY count DESC LIMIT 100" + ).all(); + res.json({ total, years, cities, starred: 0, tags: [], collections: [] }); +}); + +app.get('/api/videos', (req, res) => { + const { from, to, city } = req.query; + const conditions = ["p.media_type = 'video'"]; + const params = []; + if (from) { conditions.push('p.date_taken >= ?'); params.push(from); } + if (to) { conditions.push('p.date_taken <= ?'); params.push(to); } + if (city) { conditions.push('p.place_city = ?'); params.push(city); } + const where = 'WHERE ' + conditions.join(' AND '); + const rows = db.raw.prepare( + `SELECT ${SELECT_COLS} FROM photos p ${where} ORDER BY p.date_taken DESC, p.id DESC` + ).all(...params); res.json({ videos: rows.map(mapPhoto), total: rows.length }); });