From 0f941b7174b62a03dd6751b64f2a8f1a9a6c8ec5 Mon Sep 17 00:00:00 2001 From: Meik Date: Mon, 10 Nov 2025 18:00:07 +0100 Subject: [PATCH] Feat: Geolocation --- src/App.js | 54 +++--- src/components/DashboardView.js | 274 ++++++++++++++++--------------- src/components/StoreWatchPage.js | 2 +- 3 files changed, 167 insertions(+), 163 deletions(-) diff --git a/src/App.js b/src/App.js index d7998d5..0bc452b 100644 --- a/src/App.js +++ b/src/App.js @@ -92,7 +92,6 @@ function App() { authorizedFetch, bootstrapSession, performLogout, - handleUnauthorized, storeToken, getStoredToken } = useSessionManager({ @@ -456,31 +455,34 @@ function App() { } }; - const handleDateRangeSelection = useCallback((entryId, startDate, endDate) => { - setIsDirty(true); - setConfig((prev) => - prev.map((item) => { - if (item.id !== entryId) { - return item; - } - const updated = { ...item }; - const startValue = formatDateValue(startDate); - const endValue = formatDateValue(endDate); - if (startValue || endValue) { - updated.desiredDateRange = { - start: startValue || endValue, - end: endValue || startValue - }; - } else if (updated.desiredDateRange) { - delete updated.desiredDateRange; - } - if (updated.desiredDate) { - delete updated.desiredDate; - } - return updated; - }) - ); - }, [setConfig]); + const handleDateRangeSelection = useCallback( + (entryId, startDate, endDate) => { + setIsDirty(true); + setConfig((prev) => + prev.map((item) => { + if (item.id !== entryId) { + return item; + } + const updated = { ...item }; + const startValue = formatDateValue(startDate); + const endValue = formatDateValue(endDate); + if (startValue || endValue) { + updated.desiredDateRange = { + start: startValue || endValue, + end: endValue || startValue + }; + } else if (updated.desiredDateRange) { + delete updated.desiredDateRange; + } + if (updated.desiredDate) { + delete updated.desiredDate; + } + return updated; + }) + ); + }, + [setConfig, setIsDirty] + ); const configMap = useMemo(() => { const map = new Map(); diff --git a/src/components/DashboardView.js b/src/components/DashboardView.js index 0dbfb42..7a2963f 100644 --- a/src/components/DashboardView.js +++ b/src/components/DashboardView.js @@ -16,14 +16,144 @@ const ColumnTextFilter = ({ column, placeholder }) => { if (!column.getCanFilter()) { return null; } - const configTableData = useMemo(() => { - return Array.isArray(visibleConfig) - ? visibleConfig.map((item) => ({ - ...item, - normalizedLabel: (item.label || '').toLowerCase() - })) - : []; - }, [visibleConfig]); + return ( + column.setFilterValue(event.target.value || undefined)} + placeholder={placeholder} + className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + ); +}; + +const ColumnSelectFilter = ({ column, options, placeholder = 'Alle' }) => { + if (!column.getCanFilter()) { + return null; + } + return ( + + ); +}; + +function readConfigTableState() { + if (typeof window === 'undefined') { + return { sorting: [], columnFilters: [] }; + } + try { + const raw = window.localStorage.getItem(CONFIG_TABLE_STATE_KEY); + if (!raw) { + return { sorting: [], columnFilters: [] }; + } + const parsed = JSON.parse(raw); + return { + sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [], + columnFilters: Array.isArray(parsed.columnFilters) ? parsed.columnFilters : [] + }; + } catch { + return { sorting: [], columnFilters: [] }; + } +} + +function persistConfigTableState(state) { + if (typeof window === 'undefined') { + return; + } + try { + window.localStorage.setItem(CONFIG_TABLE_STATE_KEY, JSON.stringify(state)); + } catch { + /* ignore */ + } +} + +const DashboardView = ({ + session, + onRefresh, + onLogout, + notificationPanelOpen, + onToggleNotificationPanel, + notificationProps, + stores, + availableCollapsed, + onToggleStores, + onStoreSelect, + configMap, + error, + onDismissError, + status, + visibleConfig, + config, + onToggleActive, + onToggleProfileCheck, + onToggleOnlyNotify, + onWeekdayChange, + weekdays, + onRangePickerRequest, + formatRangeLabel, + onSaveConfig, + onResetConfig, + onHideEntry, + onDeleteEntry, + canDelete, + focusedStoreId, + onClearFocus, + userLocation, + locationLoading, + locationSaving, + locationError, + onUpdateLocation +}) => { + useEffect(() => { + if (!focusedStoreId) { + return; + } + const row = document.querySelector(`[data-store-row="${focusedStoreId}"]`); + if (!row) { + onClearFocus(); + return; + } + row.scrollIntoView({ behavior: 'smooth', block: 'center' }); + row.classList.add('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); + const timeout = setTimeout(() => { + row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); + onClearFocus(); + }, 2500); + return () => { + clearTimeout(timeout); + row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); + }; + }, [focusedStoreId, onClearFocus]); + const [geoBusy, setGeoBusy] = useState(false); + const [geoError, setGeoError] = useState(''); + const initialTableState = useMemo(() => readConfigTableState(), []); + const [tableSorting, setTableSorting] = useState(initialTableState.sorting); + const [tableFilters, setTableFilters] = useState(initialTableState.columnFilters); + + useEffect(() => { + persistConfigTableState({ sorting: tableSorting, columnFilters: tableFilters }); + }, [tableSorting, tableFilters]); + + const configTableData = useMemo( + () => + Array.isArray(visibleConfig) + ? visibleConfig.map((item) => ({ + ...item, + normalizedLabel: (item.label || '').toLowerCase() + })) + : [], + [visibleConfig] + ); const weekdaysOptions = useMemo( () => @@ -251,134 +381,6 @@ const ColumnTextFilter = ({ column, placeholder }) => { getFilteredRowModel: getFilteredRowModel() }); - return ( - column.setFilterValue(event.target.value || undefined)} - placeholder={placeholder} - className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500" - /> - ); -}; - -const ColumnSelectFilter = ({ column, options, placeholder = 'Alle' }) => { - if (!column.getCanFilter()) { - return null; - } - return ( - - ); -}; - -const DashboardView = ({ - session, - onRefresh, - onLogout, - notificationPanelOpen, - onToggleNotificationPanel, - notificationProps, - stores, - availableCollapsed, - onToggleStores, - onStoreSelect, - configMap, - error, - onDismissError, - status, - visibleConfig, - config, - onToggleActive, - onToggleProfileCheck, - onToggleOnlyNotify, - onWeekdayChange, - weekdays, - onRangePickerRequest, - formatRangeLabel, - onSaveConfig, - onResetConfig, - onHideEntry, - onDeleteEntry, - canDelete, - focusedStoreId, - onClearFocus, - userLocation, - locationLoading, - locationSaving, - locationError, - onUpdateLocation -}) => { - const loadTableState = useCallback(() => { - if (typeof window === 'undefined') { - return { sorting: [], columnFilters: [] }; - } - try { - const raw = window.localStorage.getItem(CONFIG_TABLE_STATE_KEY); - if (!raw) { - return { sorting: [], columnFilters: [] }; - } - const parsed = JSON.parse(raw); - return { - sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [], - columnFilters: Array.isArray(parsed.columnFilters) ? parsed.columnFilters : [] - }; - } catch { - return { sorting: [], columnFilters: [] }; - } - }, []); - - const initialTableState = loadTableState(); - - useEffect(() => { - if (!focusedStoreId) { - return; - } - const row = document.querySelector(`[data-store-row="${focusedStoreId}"]`); - if (!row) { - onClearFocus(); - return; - } - row.scrollIntoView({ behavior: 'smooth', block: 'center' }); - row.classList.add('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); - const timeout = setTimeout(() => { - row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); - onClearFocus(); - }, 2500); - return () => { - clearTimeout(timeout); - row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400'); - }; - }, [focusedStoreId, onClearFocus]); - const [geoBusy, setGeoBusy] = useState(false); - const [geoError, setGeoError] = useState(''); - const [tableSorting, setTableSorting] = useState(initialTableState.sorting); - const [tableFilters, setTableFilters] = useState(initialTableState.columnFilters); - - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - try { - window.localStorage.setItem( - CONFIG_TABLE_STATE_KEY, - JSON.stringify({ sorting: tableSorting, columnFilters: tableFilters }) - ); - } catch { - /* ignore */ - } - }, [tableSorting, tableFilters]); - const handleDetectLocation = useCallback(() => { if (!navigator.geolocation) { setGeoError('Standortbestimmung wird von diesem Browser nicht unterstützt.'); diff --git a/src/components/StoreWatchPage.js b/src/components/StoreWatchPage.js index 16571ef..66a464f 100644 --- a/src/components/StoreWatchPage.js +++ b/src/components/StoreWatchPage.js @@ -500,7 +500,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) => } finally { setRegionLoading(false); } - }, [authorizedFetch, selectedRegionId]); + }, [authorizedFetch]); const loadSubscriptions = useCallback(async () => { if (!authorizedFetch) {