From 9a3903b7612c072f72ce49b0ed213ea9ec325282 Mon Sep 17 00:00:00 2001
From: Meik
Date: Mon, 10 Nov 2025 21:30:43 +0100
Subject: [PATCH] minor changes
---
src/App.js | 52 +++++++++++-------
src/components/NotificationPanel.js | 6 ++-
src/components/StoreWatchPage.js | 83 ++++++++++++++++++++++++++---
src/utils/locationLabel.js | 65 ++++++++++++++++++++++
4 files changed, 181 insertions(+), 25 deletions(-)
create mode 100644 src/utils/locationLabel.js
diff --git a/src/App.js b/src/App.js
index 3758f48..6f415be 100644
--- a/src/App.js
+++ b/src/App.js
@@ -5,6 +5,7 @@ import './App.css';
import 'react-date-range/dist/styles.css';
import 'react-date-range/dist/theme/default.css';
import { formatDateValue, formatRangeLabel } from './utils/dateUtils';
+import { inferLocationLabel } from './utils/locationLabel';
import useSyncProgress from './hooks/useSyncProgress';
import useNotificationSettings from './hooks/useNotificationSettings';
import useConfigManager from './hooks/useConfigManager';
@@ -634,6 +635,31 @@ function App() {
);
}
+ const userLocationWithLabel = useMemo(() => {
+ if (!preferences?.location) {
+ return null;
+ }
+ const label = inferLocationLabel(preferences.location, stores);
+ return label ? { ...preferences.location, label } : { ...preferences.location };
+ }, [preferences?.location, stores]);
+
+ const sharedNotificationProps = {
+ error: notificationError,
+ message: notificationMessage,
+ settings: notificationSettings,
+ capabilities: notificationCapabilities,
+ loading: notificationLoading,
+ dirty: notificationDirty,
+ saving: notificationSaving,
+ onReset: loadNotificationSettings,
+ onSave: saveNotificationSettings,
+ onFieldChange: handleNotificationFieldChange,
+ onSendTest: sendNotificationTest,
+ onCopyLink: copyToClipboard,
+ copyFeedback,
+ ntfyPreviewUrl
+ };
+
const dashboardContent = (
setNotificationPanelOpen((prev) => !prev)}
- notificationProps={{
- error: notificationError,
- message: notificationMessage,
- settings: notificationSettings,
- capabilities: notificationCapabilities,
- loading: notificationLoading,
- dirty: notificationDirty,
- saving: notificationSaving,
- onReset: loadNotificationSettings,
- onSave: saveNotificationSettings,
- onFieldChange: handleNotificationFieldChange,
- onSendTest: sendNotificationTest,
- onCopyLink: copyToClipboard,
- copyFeedback,
- ntfyPreviewUrl
- }}
+ notificationProps={sharedNotificationProps}
stores={stores}
availableCollapsed={availableCollapsed}
onToggleStores={() => setAvailableCollapsed((prev) => !prev)}
@@ -681,7 +692,7 @@ function App() {
canDelete={Boolean(session?.isAdmin)}
focusedStoreId={focusedStoreId}
onClearFocus={() => setFocusedStoreId(null)}
- userLocation={preferences?.location || null}
+ userLocation={userLocationWithLabel}
locationLoading={preferencesLoading}
locationSaving={locationSaving}
locationError={preferencesError}
@@ -721,9 +732,14 @@ function App() {
setNotificationPanelOpen((prev) => !prev)}
+ notificationProps={sharedNotificationProps}
/>
}
/>
diff --git a/src/components/NotificationPanel.js b/src/components/NotificationPanel.js
index 3dbcd89..cfb6346 100644
--- a/src/components/NotificationPanel.js
+++ b/src/components/NotificationPanel.js
@@ -211,7 +211,11 @@ const NotificationPanel = ({
Standort wird geladen...
) : location ? (
- Aktueller Standort: {location.lat.toFixed(4)}, {location.lon.toFixed(4)} (
+ Aktueller Standort: {location.lat.toFixed(4)}, {location.lon.toFixed(4)}
+ {location.label && (
+ – {location.label}
+ )}{' '}
+ (
{location.updatedAt ? new Date(location.updatedAt).toLocaleString('de-DE') : 'Zeit unbekannt'})
) : (
diff --git a/src/components/StoreWatchPage.js b/src/components/StoreWatchPage.js
index 35eaf80..704c414 100644
--- a/src/components/StoreWatchPage.js
+++ b/src/components/StoreWatchPage.js
@@ -8,6 +8,7 @@ import {
useReactTable
} from '@tanstack/react-table';
import { haversineDistanceKm } from '../utils/distance';
+import NotificationPanel from './NotificationPanel';
const REGION_STORAGE_KEY = 'storeWatchRegionSelection';
const WATCH_TABLE_STATE_KEY = 'storeWatchTableState';
@@ -92,7 +93,12 @@ const StoreWatchPage = ({
knownStores = [],
userLocation,
onRequestLocation,
- locationLoading = false
+ locationLoading = false,
+ locationSaving = false,
+ locationError = '',
+ notificationPanelOpen = false,
+ onToggleNotificationPanel = () => {},
+ notificationProps
}) => {
const [regions, setRegions] = useState([]);
const [selectedRegionId, setSelectedRegionId] = useState(() => {
@@ -120,6 +126,9 @@ const StoreWatchPage = ({
const [locationPromptTriggered, setLocationPromptTriggered] = useState(false);
const [locationPromptPending, setLocationPromptPending] = useState(false);
const [locationPromptError, setLocationPromptError] = useState('');
+ const combinedLocationError = locationError || locationPromptError;
+ const locationActionBusy = locationSaving || locationPromptPending;
+ const formatCoordinate = (value) => (Number.isFinite(value) ? Number(value).toFixed(4) : '–');
useEffect(() => {
if (typeof window === 'undefined') {
@@ -183,6 +192,16 @@ const StoreWatchPage = ({
requestLocation();
}, [userLocation, locationLoading, onRequestLocation, locationPromptTriggered, requestLocation]);
+ const handleManualDetectLocation = useCallback(() => {
+ setLocationPromptError('');
+ requestLocation();
+ }, [requestLocation]);
+
+ const handleClearStoredLocation = useCallback(async () => {
+ setLocationPromptError('');
+ await onRequestLocation?.(null);
+ }, [onRequestLocation]);
+
const watchedIds = useMemo(
() => new Set(watchList.map((entry) => String(entry.storeId))),
[watchList]
@@ -848,6 +867,58 @@ const StoreWatchPage = ({
+
+
+ {notificationProps?.loading &&
Lade…}
+
+
+ {notificationPanelOpen && notificationProps && (
+
+ )}
+
+ {userLocation && !notificationPanelOpen && (
+
+
+ Standort gespeichert:{' '}
+ {userLocation.label ? (
+ {userLocation.label}
+ ) : (
+ 'Koordinaten'
+ )}{' '}
+ • {formatCoordinate(userLocation.lat)}, {formatCoordinate(userLocation.lon)}
+
+
+ Aktualisiert:{' '}
+ {userLocation.updatedAt ? new Date(userLocation.updatedAt).toLocaleString('de-DE') : 'Zeit unbekannt'}
+
+
+ )}
+
{(error || status) && (
{error && (
@@ -863,16 +934,16 @@ const StoreWatchPage = ({
{!userLocation && !locationLoading && onRequestLocation && (
- {locationPromptPending ? (
+ {locationActionBusy ? (
Standort wird automatisch angefragt, um Entfernungen berechnen zu können...
- ) : locationPromptError ? (
+ ) : combinedLocationError ? (
-
{locationPromptError}
+
{combinedLocationError}
diff --git a/src/utils/locationLabel.js b/src/utils/locationLabel.js
new file mode 100644
index 0000000..0b9c9d5
--- /dev/null
+++ b/src/utils/locationLabel.js
@@ -0,0 +1,65 @@
+import { haversineDistanceKm } from './distance';
+
+const DEFAULT_MAX_DISTANCE_KM = 60;
+
+export function inferLocationLabel(location, stores = [], options = {}) {
+ if (
+ !location ||
+ typeof location !== 'object' ||
+ !Number.isFinite(location.lat) ||
+ !Number.isFinite(location.lon) ||
+ !Array.isArray(stores) ||
+ stores.length === 0
+ ) {
+ return null;
+ }
+
+ const maxDistanceKm = Number.isFinite(options.maxDistanceKm)
+ ? options.maxDistanceKm
+ : DEFAULT_MAX_DISTANCE_KM;
+
+ let closest = null;
+ for (const store of stores) {
+ const storeLat = Number(store?.location?.lat);
+ const storeLon = Number(store?.location?.lon);
+ if (!Number.isFinite(storeLat) || !Number.isFinite(storeLon)) {
+ continue;
+ }
+ const distance = haversineDistanceKm(location.lat, location.lon, storeLat, storeLon);
+ if (distance === null) {
+ continue;
+ }
+ if (closest && distance >= closest.distance) {
+ continue;
+ }
+ const labelParts = [];
+ if (store.city) {
+ labelParts.push(store.city);
+ }
+ if (store.region?.name) {
+ const cityNormalized = store.city?.toLowerCase();
+ const regionNormalized = store.region.name.toLowerCase();
+ if (!cityNormalized || cityNormalized !== regionNormalized) {
+ labelParts.push(store.region.name);
+ }
+ }
+ if (labelParts.length === 0 && store.name) {
+ labelParts.push(store.name);
+ }
+ const label =
+ labelParts.length > 0 ? labelParts.join(' • ') : `Store ${store.id ?? ''}`.trim();
+ closest = {
+ label,
+ distance
+ };
+ }
+
+ if (!closest) {
+ return null;
+ }
+
+ if (maxDistanceKm <= 0 || closest.distance <= maxDistanceKm) {
+ return closest.label;
+ }
+ return null;
+}