From a9da686b2eaf7606a1320d18b705b94528f7d67c Mon Sep 17 00:00:00 2001 From: Tamanna030507 Date: Wed, 3 Jun 2026 14:16:59 +0530 Subject: [PATCH 1/3] feat: add comparison navigation workflow --- src/pages/AnalysisDashboard.tsx | 44 ++++++--- src/pages/ResultsPage.tsx | 157 ++++++++++++++++++++------------ 2 files changed, 131 insertions(+), 70 deletions(-) diff --git a/src/pages/AnalysisDashboard.tsx b/src/pages/AnalysisDashboard.tsx index f6005dc..4cd7b13 100644 --- a/src/pages/AnalysisDashboard.tsx +++ b/src/pages/AnalysisDashboard.tsx @@ -23,6 +23,8 @@ function gradeColor(grade: string) { export default function AnalysisDashboard() { const [params] = useSearchParams(); const [scan, setScan] = useState(null); + const [comparisonScans, setComparisonScans] = useState([]); + const [isComparisonMode, setIsComparisonMode] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -31,15 +33,31 @@ export default function AnalysisDashboard() { setLoading(true); setError(''); try { - const idParam = params.get('id'); - const lastId = sessionStorage.getItem('lastScanId'); - const targetId = idParam || lastId; + const compareParam = params.get('compare'); + const idParam = params.get('id'); - const res = targetId - ? await api.getScan(targetId) - : await api.getLatestScan(); + if (compareParam) { + setIsComparisonMode(true); + const ids = compareParam.split(','); + + const records = await Promise.all(ids.map(id => api.getScan(id))); + const parsedScans = records.map(res => res.scan); + + setComparisonScans(parsedScans); + if (parsedScans.length > 0) { + setScan(parsedScans[0]); + } + } else { + setIsComparisonMode(false); + const lastId = sessionStorage.getItem('lastScanId'); + const targetId = idParam || lastId; - setScan(res.scan); + const res = targetId + ? await api.getScan(targetId) + : await api.getLatestScan(); + + setScan(res.scan); + } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load scan data.'); } finally { @@ -49,17 +67,15 @@ export default function AnalysisDashboard() { load(); }, [params]); - // ── Loading state ──────────────────────────────────────────────────────── if (loading) { return (
- +
); } - // ── Error state ────────────────────────────────────────────────────────── - if (error || !scan) { + if (error || (!scan && !isComparisonMode)) { return (
@@ -76,8 +92,8 @@ export default function AnalysisDashboard() { ); } - const { freshness_index, grade, confidence, classification, species, biomarkers, recommendations } = scan; - const displayId = scan.scan_display_id; + const { freshness_index, grade, confidence, classification, species, biomarkers, recommendations } = scan!; + const displayId = scan!.scan_display_id; const alerts = recommendations.alert_flags; return ( @@ -293,4 +309,4 @@ export default function AnalysisDashboard() {
); -} +} \ No newline at end of file diff --git a/src/pages/ResultsPage.tsx b/src/pages/ResultsPage.tsx index 7e84312..2804168 100644 --- a/src/pages/ResultsPage.tsx +++ b/src/pages/ResultsPage.tsx @@ -1,12 +1,16 @@ import { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { ArrowRight } from 'lucide-react'; import GlassCard from '../components/GlassCard'; import StatusTerminal from '../components/StatusTerminal'; import { api } from '../lib/api'; import type { HistoryScan, HistoryStats } from '../lib/types'; +import { useComparison } from '../context/ComparisonContext'; export default function ResultsPage() { + const navigate = useNavigate(); + const { selectedScanIds, toggleScanSelection, isComparisonReady } = useComparison(); + const [scans, setScans] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); @@ -58,7 +62,7 @@ export default function ResultsPage() { const freshRate = stats?.fresh_rate_percent ?? 0; return ( -
+
) : (
- {scans.map(h => ( - - -
-
- {/* Thumbnail */} - {h.photo_url && ( - {h.species_detected} - )} + {scans.map(h => { + const isChecked = selectedScanIds.includes(h.id); + + return ( +
+ {/* REQUIREMENT 1 & 2: Checkbox toggle placed side-by-side with scan card */} +
+ toggleScanSelection(h.id)} + className="w-5 h-5 accent-neon cursor-pointer bg-surface-mid border-on-surface-variant/30 rounded focus:ring-0" + /> +
-
-
-

- {h.species_detected} -

- - {h.grade} - + + +
+
+ {/* Thumbnail */} + {h.photo_url && ( + {h.species_detected} + )} + +
+
+

+ {h.species_detected} +

+ + {h.grade} + +
+
+ + {h.scan_display_id} + + + {h.market_name} + + {h.timestamp && ( + + {new Date(h.timestamp).toLocaleString('en-IN', { + day: '2-digit', month: 'short', year: 'numeric', + hour: '2-digit', minute: '2-digit', + })} + + )} +
+
-
- - {h.scan_display_id} - - - {h.market_name} + +
+ + {h.freshness_index} - {h.timestamp && ( - - {new Date(h.timestamp).toLocaleString('en-IN', { - day: '2-digit', month: 'short', year: 'numeric', - hour: '2-digit', minute: '2-digit', - })} - - )} +
-
+
+ +
+ ); + })} +
+ )} -
- - {h.freshness_index} - - -
-
- - - ))} + {/* REQUIREMENT 3 & 4: Floating action bar to maintain selection state & conditionally enable action */} + {selectedScanIds.length > 0 && ( +
+ + SELECTED: {selectedScanIds.length}/4_SCANS + + +
)}
); -} +} \ No newline at end of file From 5226a663bffa0c410daaf8278ba024dd732d7d01 Mon Sep 17 00:00:00 2001 From: Tamanna030507 Date: Wed, 3 Jun 2026 14:51:29 +0530 Subject: [PATCH 2/3] feat: implement side-by-side scan comparison view --- src/pages/ComparePage.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/pages/ComparePage.tsx diff --git a/src/pages/ComparePage.tsx b/src/pages/ComparePage.tsx new file mode 100644 index 0000000..e69de29 From 67b38678381b886fd6e44338f7098df8f9b5c9d5 Mon Sep 17 00:00:00 2001 From: Tamanna030507 Date: Wed, 3 Jun 2026 15:49:32 +0530 Subject: [PATCH 3/3] feat: implement historical scan comparison view with structural validation --- src/App.tsx | 34 +++++++++++----------- src/context/ComparisonContext.tsx | 48 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 src/context/ComparisonContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 14f81c6..51cdd4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,30 +10,30 @@ import MarketMapPage from './pages/MarketMapPage'; import ResultsPage from './pages/ResultsPage'; import PostHogPageView from './components/PostHogPageView'; import NotFound from './pages/NotFound'; +// Import the context wrapper +import { ComparisonProvider } from './context/ComparisonContext'; export default function App() { return ( - {/* Toast provider for global error notifications */} - - {/* Fires a $pageview event to PostHog on every SPA route change */} - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* Catch-all route for broken links/404s */} - } /> - - + {/* Wrap everything cleanly inside the ComparisonProvider */} + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } \ No newline at end of file diff --git a/src/context/ComparisonContext.tsx b/src/context/ComparisonContext.tsx new file mode 100644 index 0000000..b55e945 --- /dev/null +++ b/src/context/ComparisonContext.tsx @@ -0,0 +1,48 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface ComparisonContextType { + selectedScanIds: string[]; + toggleScanSelection: (scanId: string) => void; + clearSelection: () => void; + isComparisonReady: boolean; +} + +const ComparisonContext = createContext(undefined); + +export function ComparisonProvider({ children }: { children: ReactNode }) { + const [selectedScanIds, setSelectedScanIds] = useState([]); + + const toggleScanSelection = (scanId: string) => { + setSelectedScanIds((prev) => { + if (prev.includes(scanId)) { + return prev.filter((id) => id !== scanId); + } + if (prev.length >= 4) return prev; // Limit to 4 cards for grid-view constraints + return [...prev, scanId]; + }); + }; + + const clearSelection = () => setSelectedScanIds([]); + const isComparisonReady = selectedScanIds.length >= 2; + + return ( + + {children} + + ); +} + +export function useComparison() { + const context = useContext(ComparisonContext); + if (!context) { + throw new Error('useComparison must be used within a ComparisonProvider'); + } + return context; +} \ No newline at end of file