diff --git a/server.js b/server.js index 2ded18c..eb4a7f6 100644 --- a/server.js +++ b/server.js @@ -12,7 +12,7 @@ const adminConfig = require('./services/adminConfig'); const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore'); const notificationService = require('./services/notificationService'); const { readStoreWatch, writeStoreWatch, listWatcherProfiles } = require('./services/storeWatchStore'); -const { readPreferences, writePreferences } = require('./services/userPreferencesStore'); +const { readPreferences, writePreferences, sanitizeLocation } = require('./services/userPreferencesStore'); const { readStoreStatus, writeStoreStatus } = require('./services/storeStatusStore'); const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; @@ -805,9 +805,29 @@ app.post('/api/store-watch/subscriptions', requireAuth, (req, res) => { res.json({ success: true, stores: persisted }); }); -app.get('/api/user/preferences', requireAuth, (req, res) => { +app.get('/api/user/preferences', requireAuth, async (req, res) => { const preferences = readPreferences(req.session.profile.id); - res.json(preferences); + let location = preferences.location; + try { + const details = await foodsharingClient.fetchProfile(req.session.cookieHeader); + const coords = details?.coordinates; + const sanitized = sanitizeLocation({ lat: coords?.lat, lon: coords?.lon }); + if (sanitized) { + const city = details?.city?.trim() || ''; + location = { + ...sanitized, + label: city || preferences.location?.label || 'Foodsharing-Profil', + city, + updatedAt: sanitized.updatedAt + }; + } + } catch (error) { + console.error('[PREFERENCES] Profilstandort konnte nicht geladen werden:', error.message); + } + res.json({ + ...preferences, + location + }); }); app.post('/api/user/preferences/location', requireAuth, (req, res) => { diff --git a/services/userPreferencesStore.js b/services/userPreferencesStore.js index ed1bc23..b68e09b 100644 --- a/services/userPreferencesStore.js +++ b/services/userPreferencesStore.js @@ -71,5 +71,6 @@ function writePreferences(profileId, patch = {}) { module.exports = { readPreferences, - writePreferences + writePreferences, + sanitizeLocation }; diff --git a/src/App.js b/src/App.js index daebe9c..45b0d3a 100644 --- a/src/App.js +++ b/src/App.js @@ -120,9 +120,7 @@ function App() { const { preferences, loading: preferencesLoading, - saving: locationSaving, - error: preferencesError, - updateLocation + error: preferencesError } = useUserPreferences({ authorizedFetch, sessionToken: session?.token @@ -747,9 +745,7 @@ function App() { onClearFocus={() => setFocusedStoreId(null)} userLocation={userLocationWithLabel} locationLoading={preferencesLoading} - locationSaving={locationSaving} locationError={preferencesError} - onUpdateLocation={updateLocation} /> ); @@ -782,14 +778,12 @@ function App() { setNotificationPanelOpen((prev) => !prev)} notificationProps={sharedNotificationProps} diff --git a/src/components/DashboardView.js b/src/components/DashboardView.js index e565604..72f2f8b 100644 --- a/src/components/DashboardView.js +++ b/src/components/DashboardView.js @@ -110,9 +110,7 @@ const DashboardView = ({ onClearFocus, userLocation, locationLoading, - locationSaving, - locationError, - onUpdateLocation + locationError }) => { useEffect(() => { if (!focusedStoreId) { @@ -134,8 +132,6 @@ const DashboardView = ({ 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); @@ -396,41 +392,6 @@ const DashboardView = ({ getFilteredRowModel: getFilteredRowModel() }); - const handleDetectLocation = useCallback(() => { - if (!navigator.geolocation) { - setGeoError('Standortbestimmung wird von diesem Browser nicht unterstützt.'); - return; - } - setGeoBusy(true); - setGeoError(''); - navigator.geolocation.getCurrentPosition( - async (position) => { - const lat = position.coords.latitude; - const lon = position.coords.longitude; - await onUpdateLocation?.({ lat, lon }); - setGeoBusy(false); - }, - (error) => { - setGeoBusy(false); - setGeoError( - error.code === error.PERMISSION_DENIED - ? 'Zugriff auf den Standort wurde verweigert.' - : 'Standort konnte nicht ermittelt werden.' - ); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - } - ); - }, [onUpdateLocation]); - - const handleClearLocation = useCallback(async () => { - setGeoError(''); - await onUpdateLocation?.(null); - }, [onUpdateLocation]); - const { error: notificationError, message: notificationMessage, @@ -533,13 +494,7 @@ const DashboardView = ({ ntfyPreviewUrl={ntfyPreviewUrl} location={userLocation} locationLoading={locationLoading} - locationSaving={locationSaving || geoBusy} - locationError={locationError || geoError} - onDetectLocation={() => { - setGeoError(''); - handleDetectLocation(); - }} - onClearLocation={handleClearLocation} + locationError={locationError} /> )} diff --git a/src/components/NotificationPanel.js b/src/components/NotificationPanel.js index 153405e..3179dd4 100644 --- a/src/components/NotificationPanel.js +++ b/src/components/NotificationPanel.js @@ -15,10 +15,7 @@ const NotificationPanel = ({ ntfyPreviewUrl, location, locationLoading, - locationSaving, - locationError, - onDetectLocation, - onClearLocation + locationError }) => { return (
@@ -172,29 +169,10 @@ const NotificationPanel = ({

Standort für Entfernungssortierung

- Der Standort wird für die Entfernungskalkulation im Monitoring genutzt. Nur für dich sichtbar. + Der Standort wird für die Entfernungskalkulation im Monitoring genutzt. Nur für dich sichtbar. Die Daten + stammen direkt aus deinem Foodsharing-Profil.

-
- - {location && ( - - )} -
{locationLoading ? ( @@ -216,7 +194,7 @@ const NotificationPanel = ({ )}
) : ( -

Kein Standort hinterlegt.

+

Standortdaten werden direkt aus deinem Foodsharing-Profil übernommen.

)} {locationError &&

{locationError}

} diff --git a/src/components/StoreWatchPage.js b/src/components/StoreWatchPage.js index ad76b68..e3a6692 100644 --- a/src/components/StoreWatchPage.js +++ b/src/components/StoreWatchPage.js @@ -93,9 +93,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation, - onRequestLocation, locationLoading = false, - locationSaving = false, locationError = '', notificationPanelOpen = false, onToggleNotificationPanel = () => {}, @@ -125,12 +123,6 @@ const StoreWatchPage = ({ const initialTableState = useMemo(() => readWatchTableState(), []); const [sorting, setSorting] = useState(initialTableState.sorting); const [columnFilters, setColumnFilters] = useState(initialTableState.columnFilters); - 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) : '–'); const aggregatedRegionStores = useMemo(() => { const list = []; Object.values(storesByRegion).forEach((entry) => { @@ -182,63 +174,6 @@ const StoreWatchPage = ({ persistWatchTableState({ sorting, columnFilters }); }, [sorting, columnFilters]); - const requestLocation = useCallback(() => { - if (!onRequestLocation) { - return; - } - setLocationPromptTriggered(true); - if (typeof window === 'undefined' || typeof navigator === 'undefined' || !navigator.geolocation) { - setLocationPromptPending(false); - setLocationPromptError('Standortbestimmung wird von diesem Browser nicht unterstützt.'); - return; - } - setLocationPromptPending(true); - setLocationPromptError(''); - navigator.geolocation.getCurrentPosition( - async (position) => { - setLocationPromptPending(false); - try { - await onRequestLocation({ - lat: position.coords.latitude, - lon: position.coords.longitude - }); - } catch { - setLocationPromptError('Standort konnte nicht gespeichert werden.'); - } - }, - (geoError) => { - setLocationPromptPending(false); - setLocationPromptError( - geoError.code === geoError.PERMISSION_DENIED - ? 'Zugriff auf den Standort wurde verweigert. Bitte erlaube den Zugriff im Browser.' - : 'Standort konnte nicht automatisch ermittelt werden.' - ); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - } - ); - }, [onRequestLocation]); - - useEffect(() => { - if (userLocation || locationLoading || !onRequestLocation || locationPromptTriggered) { - return; - } - 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] @@ -964,10 +899,7 @@ const StoreWatchPage = ({ {...notificationProps} location={displayLocation} locationLoading={locationLoading} - locationSaving={locationActionBusy} - locationError={combinedLocationError} - onDetectLocation={handleManualDetectLocation} - onClearLocation={handleClearStoredLocation} + locationError={locationError} /> )} @@ -984,25 +916,12 @@ const StoreWatchPage = ({ )} - {!userLocation && !locationLoading && onRequestLocation && ( + {!userLocation && !locationLoading && (
- {locationActionBusy ? ( -

Standort wird automatisch angefragt, um Entfernungen berechnen zu können...

- ) : combinedLocationError ? ( -
-

{combinedLocationError}

- -
- ) : ( -

Bitte bestätige die Standortabfrage deines Browsers für Entfernungssortierung.

- )} +

+ Standortdaten für die Entfernungssortierung stammen automatisch aus deinem Foodsharing-Profil. Ergänze + dort deine Koordinaten, falls sie fehlen. +

)} diff --git a/src/hooks/useUserPreferences.js b/src/hooks/useUserPreferences.js index 756512d..2c18a03 100644 --- a/src/hooks/useUserPreferences.js +++ b/src/hooks/useUserPreferences.js @@ -1,12 +1,9 @@ import { useCallback, useEffect, useState } from 'react'; const PREFERENCES_ENDPOINT = '/api/user/preferences'; -const LOCATION_ENDPOINT = '/api/user/preferences/location'; - const useUserPreferences = ({ authorizedFetch, sessionToken }) => { const [preferences, setPreferences] = useState(null); const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const loadPreferences = useCallback(async () => { @@ -30,42 +27,6 @@ const useUserPreferences = ({ authorizedFetch, sessionToken }) => { } }, [authorizedFetch, sessionToken]); - const updateLocation = useCallback( - async (coords) => { - if (!sessionToken || !authorizedFetch) { - return false; - } - setSaving(true); - setError(''); - try { - const payload = - coords && typeof coords.lat === 'number' && typeof coords.lon === 'number' - ? { lat: coords.lat, lon: coords.lon } - : { lat: null, lon: null }; - const response = await authorizedFetch(LOCATION_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - const data = await response.json(); - setPreferences((prev) => ({ - ...(prev || {}), - location: data.location || null - })); - return true; - } catch (err) { - setError(`Standort konnte nicht aktualisiert werden: ${err.message}`); - return false; - } finally { - setSaving(false); - } - }, - [authorizedFetch, sessionToken] - ); - useEffect(() => { if (sessionToken) { loadPreferences(); @@ -77,10 +38,8 @@ const useUserPreferences = ({ authorizedFetch, sessionToken }) => { return { preferences, loading, - saving, error, - loadPreferences, - updateLocation + loadPreferences }; };