Replace Timeline-based grouped views (tags, colors, rated) with dedicated card-grid components that drill into Timeline detail views on click/Enter. Adds shared useCardGridNav hook for arrow-key navigation across all four card grids (tags, colors, rated, people). - TagsView, ColorsView, RatedView: card grid → inline Timeline detail - PeopleView: migrated to same pattern (Timeline replaces custom grid) - Tags endpoint: fall back to first associated photo for representative - Filter store: add ratingMax for exact rating filtering in RatedView - Timeline: remove tag/rating/color grouping; skip date headers when groupBy != 'date' so detail views render flat grids - SettingsDialog: bump z-index above Leaflet map layers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
import { useState } from 'react'
|
|
import { Timeline } from './components/timeline/Timeline'
|
|
import { DuplicatesView } from './components/duplicates/DuplicatesView'
|
|
import { MapView } from './components/map/MapView'
|
|
import { PeopleView } from './components/people/PeopleView'
|
|
import { TagsView } from './components/tags/TagsView'
|
|
import { ColorsView } from './components/colors/ColorsView'
|
|
import { RatedView } from './components/rated/RatedView'
|
|
import { LeftSidebar } from './components/layout/LeftSidebar'
|
|
import { RightSidebar } from './components/layout/RightSidebar'
|
|
import { TopBar } from './components/layout/TopBar'
|
|
import { ScanProgress } from './components/ScanProgress'
|
|
import { ToastContainer } from './components/ToastContainer'
|
|
import { KeyboardHints } from './components/KeyboardHints'
|
|
import { PreviewView } from './components/preview/PreviewView'
|
|
import { FilterBar } from './components/filter/FilterBar'
|
|
import { DiscardActionBar } from './components/discard/DiscardActionBar'
|
|
import { SettingsDialog } from './components/dialogs/SettingsDialog'
|
|
import { usePhotoStore } from './store/photoStore'
|
|
import { useFilterStore } from './store/filterStore'
|
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
|
import { useFilterUrlSync } from './hooks/useFilterUrlSync'
|
|
import { usePhotosQuery } from './hooks/usePhotosQuery'
|
|
|
|
function App() {
|
|
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true)
|
|
const [rightSidebarOpen, setRightSidebarOpen] = useState(true)
|
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
const viewMode = usePhotoStore((state) => state.viewMode)
|
|
const currentSection = useFilterStore((s) => s.currentSection)
|
|
|
|
// Bidirectional sync of filter store with URL query params.
|
|
useFilterUrlSync()
|
|
|
|
// Subscribe to the same photos query the Timeline uses, so the keyboard
|
|
// "open preview on first photo" path can read from the live cache regardless
|
|
// of what filter key it's stored under.
|
|
const { data: allPhotos } = usePhotosQuery()
|
|
|
|
// Set up global keyboard shortcuts
|
|
useKeyboardShortcuts({
|
|
onToggleLeftSidebar: () => setLeftSidebarOpen(!leftSidebarOpen),
|
|
onToggleRightSidebar: () => setRightSidebarOpen(!rightSidebarOpen),
|
|
getFirstPhotoId: () => allPhotos?.[0]?.id ?? null,
|
|
})
|
|
|
|
// Right sidebar stays open by default and shows whatever's selected
|
|
// (or an empty state if nothing is). User can still toggle it manually.
|
|
// Note: deliberately NOT gated on viewMode — the preview overlay sits
|
|
// on top with z-[1000], so leaving the sidebar mounted underneath
|
|
// costs nothing visually and avoids the collapse-then-reopen layout
|
|
// shift the user would otherwise see every time they exit preview.
|
|
const showRightSidebar = rightSidebarOpen
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-bg text-text">
|
|
<TopBar
|
|
leftSidebarOpen={leftSidebarOpen}
|
|
rightSidebarOpen={showRightSidebar}
|
|
onExpandLeft={() => setLeftSidebarOpen(true)}
|
|
onExpandRight={() => setRightSidebarOpen(true)}
|
|
/>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Left Sidebar */}
|
|
<div
|
|
className={`transition-all duration-200 ${
|
|
leftSidebarOpen ? 'w-60' : 'w-0'
|
|
} overflow-hidden border-r border-border bg-surface`}
|
|
>
|
|
<LeftSidebar
|
|
onCollapse={() => setLeftSidebarOpen(false)}
|
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Main column — filter bar, discard bar, timeline. Lives to the
|
|
* right of the left sidebar so the filter row doesn't bleed
|
|
* across the sidebar. relative so the KeyboardHints overlay
|
|
* centers against this column, not the viewport. */}
|
|
<div className="relative flex min-w-0 flex-1 flex-col">
|
|
<FilterBar />
|
|
<DiscardActionBar />
|
|
<div className="flex-1 overflow-auto">
|
|
{/* Section-level routing. The Map view replaces the timeline
|
|
* with a Leaflet map of GPS-tagged photos; Duplicates gets its
|
|
* own grouped grid; everything else falls through to the
|
|
* filter-driven Timeline. */}
|
|
{currentSection === 'map' ? (
|
|
<MapView />
|
|
) : currentSection === 'duplicates' ? (
|
|
<DuplicatesView />
|
|
) : currentSection === 'people' ? (
|
|
<PeopleView />
|
|
) : currentSection === 'tags' ? (
|
|
<TagsView />
|
|
) : currentSection === 'colors' ? (
|
|
<ColorsView />
|
|
) : currentSection === 'rated' ? (
|
|
<RatedView />
|
|
) : (
|
|
<Timeline />
|
|
)}
|
|
</div>
|
|
{/* Floating keyboard hints — bottom-center of the main column,
|
|
* glassy. Mounted here so it's centered against the timeline,
|
|
* not the viewport (which would be offset by the sidebars). */}
|
|
<KeyboardHints />
|
|
</div>
|
|
|
|
{/* Right Sidebar */}
|
|
<div
|
|
className={`transition-all duration-200 ${
|
|
showRightSidebar ? 'w-72' : 'w-0'
|
|
} overflow-hidden border-l border-border bg-surface`}
|
|
>
|
|
<RightSidebar onCollapse={() => setRightSidebarOpen(false)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Scan Progress Indicator */}
|
|
<ScanProgress />
|
|
|
|
{/* Toast Notifications */}
|
|
<ToastContainer />
|
|
|
|
{/* Preview overlay — covers TopBar when active */}
|
|
{viewMode === 'preview' && <PreviewView />}
|
|
|
|
{/* Settings panel — admin/maintenance actions */}
|
|
<SettingsDialog
|
|
isOpen={settingsOpen}
|
|
onClose={() => setSettingsOpen(false)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App |