diff --git a/src/components/Comparison.js b/src/components/Comparison.js index 171924e..e51685b 100644 --- a/src/components/Comparison.js +++ b/src/components/Comparison.js @@ -1,4 +1,5 @@ import React, { useState, useMemo, useCallback, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Line, Bar } from 'react-chartjs-2'; import { EmptyState, FilterControls } from './shared'; import { chartColors, createBaseOptions } from '../config/chartConfig'; @@ -38,17 +39,77 @@ const generatePresetDates = (year) => ({ }); function Comparison({ data, showDataLabels }) { + const [searchParams, setSearchParams] = useSearchParams(); + // Get latest year from data for default presets const latestYear = useMemo(() => getLatestYear(data), [data]); + const presetDates = useMemo(() => generatePresetDates(latestYear), [latestYear]); + + // Initialize state from URL or defaults + const [preset, setPresetState] = useState(() => searchParams.get('preset') || 'jan'); + const [startDate, setStartDateState] = useState(() => { + const urlPreset = searchParams.get('preset'); + if (urlPreset && urlPreset !== 'custom' && presetDates[urlPreset]) { + return presetDates[urlPreset].start; + } + return searchParams.get('from') || `${latestYear}-01-01`; + }); + const [endDate, setEndDateState] = useState(() => { + const urlPreset = searchParams.get('preset'); + if (urlPreset && urlPreset !== 'custom' && presetDates[urlPreset]) { + return presetDates[urlPreset].end; + } + return searchParams.get('to') || `${latestYear}-01-31`; + }); + const [filters, setFiltersState] = useState(() => ({ + district: searchParams.get('district') || 'all', + museum: searchParams.get('museum') || 'all' + })); - const [preset, setPreset] = useState('jan'); - const [startDate, setStartDate] = useState(`${latestYear}-01-01`); - const [endDate, setEndDate] = useState(`${latestYear}-01-31`); - const [filters, setFilters] = useState({ district: 'all', museum: 'all' }); const [chartMetric, setChartMetric] = useState('revenue'); const [chartGranularity, setChartGranularity] = useState('week'); const [activeChart, setActiveChart] = useState(0); const [activeCard, setActiveCard] = useState(0); + + // Update URL with current state + const updateUrl = useCallback((newPreset, newFrom, newTo, newFilters) => { + const params = new URLSearchParams(); + if (newPreset && newPreset !== 'jan') params.set('preset', newPreset); + if (newPreset === 'custom') { + if (newFrom) params.set('from', newFrom); + if (newTo) params.set('to', newTo); + } + if (newFilters?.district && newFilters.district !== 'all') params.set('district', newFilters.district); + if (newFilters?.museum && newFilters.museum !== 'all') params.set('museum', newFilters.museum); + setSearchParams(params, { replace: true }); + }, [setSearchParams]); + + const setPreset = (value) => { + setPresetState(value); + if (value !== 'custom' && presetDates[value]) { + setStartDateState(presetDates[value].start); + setEndDateState(presetDates[value].end); + updateUrl(value, null, null, filters); + } + }; + + const setStartDate = (value) => { + setStartDateState(value); + setPresetState('custom'); + updateUrl('custom', value, endDate, filters); + }; + + const setEndDate = (value) => { + setEndDateState(value); + setPresetState('custom'); + updateUrl('custom', startDate, value, filters); + }; + + const setFilters = (newFilters) => { + const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; + setFiltersState(updated); + updateUrl(preset, startDate, endDate, updated); + }; const charts = [ { id: 'timeseries', label: 'Trend' }, @@ -103,17 +164,6 @@ function Comparison({ data, showDataLabels }) { const districtMuseumMap = useMemo(() => getDistrictMuseumMap(data), [data]); const availableMuseums = useMemo(() => getMuseumsForDistrict(districtMuseumMap, filters.district), [districtMuseumMap, filters.district]); - // Generate presets based on latest year - const presetDates = useMemo(() => generatePresetDates(latestYear), [latestYear]); - - const handlePresetChange = (newPreset) => { - setPreset(newPreset); - if (newPreset !== 'custom' && presetDates[newPreset]) { - setStartDate(presetDates[newPreset].start); - setEndDate(presetDates[newPreset].end); - } - }; - // Year-over-year comparison: same dates, previous year const ranges = useMemo(() => ({ curr: { start: startDate, end: endDate }, @@ -341,7 +391,7 @@ function Comparison({ data, showDataLabels }) { - setPreset(e.target.value)}> diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 48c86f4..5423ef5 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -1,4 +1,5 @@ import React, { useState, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Line, Doughnut, Bar } from 'react-chartjs-2'; import { Carousel, EmptyState, FilterControls, StatCard } from './shared'; import { chartColors, createBaseOptions } from '../config/chartConfig'; @@ -24,8 +25,35 @@ const defaultFilters = { quarter: 'all' }; +const filterKeys = ['year', 'district', 'museum', 'quarter']; + function Dashboard({ data, showDataLabels }) { - const [filters, setFilters] = useState(defaultFilters); + const [searchParams, setSearchParams] = useSearchParams(); + + // Initialize filters from URL or defaults + const [filters, setFiltersState] = useState(() => { + const initial = { ...defaultFilters }; + filterKeys.forEach(key => { + const value = searchParams.get(key); + if (value) initial[key] = value; + }); + return initial; + }); + + // Update both state and URL + const setFilters = (newFilters) => { + const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; + setFiltersState(updated); + + const params = new URLSearchParams(); + filterKeys.forEach(key => { + if (updated[key] && updated[key] !== 'all') { + params.set(key, updated[key]); + } + }); + setSearchParams(params, { replace: true }); + }; + const [activeStatCard, setActiveStatCard] = useState(0); const [activeChart, setActiveChart] = useState(0); const [trendGranularity, setTrendGranularity] = useState('week'); diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..4ea4611 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1 @@ +export { useUrlState } from './useUrlState'; diff --git a/src/hooks/useUrlState.js b/src/hooks/useUrlState.js new file mode 100644 index 0000000..ad17983 --- /dev/null +++ b/src/hooks/useUrlState.js @@ -0,0 +1,58 @@ +import { useEffect, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +/** + * Sync state with URL search params + * @param {Object} state - Current state object + * @param {Function} setState - State setter function + * @param {Object} defaultState - Default state values + * @param {Array} keys - Keys to sync with URL + */ +export function useUrlState(state, setState, defaultState, keys) { + const [searchParams, setSearchParams] = useSearchParams(); + + // Initialize state from URL on mount + useEffect(() => { + const urlState = {}; + let hasUrlParams = false; + + keys.forEach(key => { + const value = searchParams.get(key); + if (value !== null) { + urlState[key] = value; + hasUrlParams = true; + } + }); + + if (hasUrlParams) { + setState(prev => ({ ...prev, ...urlState })); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Update URL when state changes + const updateUrl = useCallback((newState) => { + const params = new URLSearchParams(); + + keys.forEach(key => { + const value = newState[key]; + if (value && value !== defaultState[key]) { + params.set(key, value); + } + }); + + setSearchParams(params, { replace: true }); + }, [keys, defaultState, setSearchParams]); + + // Wrap setState to also update URL + const setStateWithUrl = useCallback((updater) => { + setState(prev => { + const newState = typeof updater === 'function' ? updater(prev) : updater; + updateUrl(newState); + return newState; + }); + }, [setState, updateUrl]); + + return setStateWithUrl; +} + +export default useUrlState;