fix(frontend): Reset-Link auswerten, Absturz bei defektem localStorage verhindern
- PasswordReset las den Query-Parameter token nie aus. Wer auf den Link
in der Reset-Mail klickte, landete wieder auf Schritt 1 mit leerem
Token-Feld — der Mailversand aus d9fecb6 lief damit ins Leere. Der
Token wird jetzt aus der URL übernommen und direkt Schritt 2 gezeigt.
- "Zurück zum Login" und die Weiterleitung nach dem Zurücksetzen zeigten
auf "/" und damit beim Unterpfad-Deployment auf das Portal.
- PublicUserList rief JSON.parse(localStorage.getItem(...)) ungeschützt
im Render-Pfad auf. Ein beschädigter Eintrag — oder ein Browser, der
Site-Data blockiert — ließ die gesamte öffentliche Liste weiß werden.
Lesen und Schreiben laufen jetzt über try/catch, und der Wert wird auf
Plausibilität geprüft.
- Der Typfilter-Effekt feuerte auch beim Mounten, obwohl useUsers bereits
selbst lädt: jeder Aufruf der öffentlichen Liste setzte zwei identische
Anfragen ab. Der erste Lauf wird jetzt übersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
868699a1bc
commit
f4939ef6c5
|
|
@ -1,11 +1,29 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
||||||
|
import { BASE_PATH } from '../../utils/constants';
|
||||||
import './PasswordReset.css';
|
import './PasswordReset.css';
|
||||||
|
|
||||||
|
// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst.
|
||||||
|
// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal.
|
||||||
|
const APP_HOME = `${BASE_PATH}/`;
|
||||||
|
|
||||||
|
// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…).
|
||||||
|
// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1
|
||||||
|
// mit leerem Token-Feld – der Mailversand war damit wirkungslos.
|
||||||
|
const tokenFromUrl = () => {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
try {
|
||||||
|
return new URLSearchParams(window.location.search).get('token') || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function PasswordReset() {
|
function PasswordReset() {
|
||||||
const [step, setStep] = useState('request'); // 'request' or 'reset'
|
const initialToken = tokenFromUrl();
|
||||||
|
const [step, setStep] = useState(initialToken ? 'reset' : 'request');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState(initialToken);
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -62,7 +80,7 @@ function PasswordReset() {
|
||||||
setToken('');
|
setToken('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
window.location.href = '/'; // Redirect to login
|
window.location.href = APP_HOME; // Redirect to login
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: result.message });
|
setMessage({ type: 'error', text: result.message });
|
||||||
|
|
@ -100,7 +118,7 @@ function PasswordReset() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<a href="/">Zurück zum Login</a>
|
<a href={APP_HOME}>Zurück zum Login</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useConfigContext } from '../../contexts/ConfigContext';
|
import { useConfigContext } from '../../contexts/ConfigContext';
|
||||||
import MapView from '../map/MapView';
|
import MapView from '../map/MapView';
|
||||||
import FilterPanel from '../users/FilterPanel';
|
import FilterPanel from '../users/FilterPanel';
|
||||||
|
|
@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers';
|
||||||
import { getGeocodeByPostalCode } from '../../services/users';
|
import { getGeocodeByPostalCode } from '../../services/users';
|
||||||
import './PublicUserList.css';
|
import './PublicUserList.css';
|
||||||
|
|
||||||
|
const LOCATION_KEY = 'userLocation';
|
||||||
|
|
||||||
|
// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt
|
||||||
|
// kann beschaedigt sein. Beides darf die Seite nicht mitreissen.
|
||||||
|
const readStoredLocation = () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LOCATION_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PublicUserList = ({ users, loading, onRefetch }) => {
|
const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
const { userTypeLabels } = useConfigContext();
|
const { userTypeLabels } = useConfigContext();
|
||||||
|
|
||||||
// Load saved location from localStorage on mount
|
|
||||||
const savedLocation = localStorage.getItem('userLocation');
|
|
||||||
const initialCoords = savedLocation ? JSON.parse(savedLocation) : null;
|
|
||||||
|
|
||||||
const [showMap, setShowMap] = useState(true);
|
const [showMap, setShowMap] = useState(true);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle');
|
// Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein
|
||||||
|
// ungeschuetztes JSON.parse im Render-Pfad: ein beschaedigter Eintrag - oder ein
|
||||||
|
// Browser, der Site-Data blockiert - hat die gesamte Liste weiss werden lassen.
|
||||||
|
const [coords, setCoords] = useState(readStoredLocation);
|
||||||
|
const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle');
|
||||||
const [locationError, setLocationError] = useState('');
|
const [locationError, setLocationError] = useState('');
|
||||||
const [coords, setCoords] = useState(initialCoords);
|
|
||||||
const [radiusKm, setRadiusKm] = useState(100);
|
const [radiusKm, setRadiusKm] = useState(100);
|
||||||
const [postalCode, setPostalCode] = useState('');
|
const [postalCode, setPostalCode] = useState('');
|
||||||
const [postalSearching, setPostalSearching] = useState(false);
|
const [postalSearching, setPostalSearching] = useState(false);
|
||||||
|
|
@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
|
|
||||||
// Save location to localStorage whenever it changes
|
// Save location to localStorage whenever it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (coords) {
|
if (!coords) return;
|
||||||
localStorage.setItem('userLocation', JSON.stringify(coords));
|
try {
|
||||||
|
localStorage.setItem(LOCATION_KEY, JSON.stringify(coords));
|
||||||
|
} catch {
|
||||||
|
// Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung.
|
||||||
}
|
}
|
||||||
}, [coords]);
|
}, [coords]);
|
||||||
|
|
||||||
|
|
@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
setFilters(prev => ({ ...prev, [key]: value }));
|
setFilters(prev => ({ ...prev, [key]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Re-fetch from backend when type filter changes (server-side filtering)
|
// Re-fetch from backend when type filter changes (server-side filtering).
|
||||||
|
// Der erste Lauf wird uebersprungen: useUsers laedt beim Mounten bereits selbst,
|
||||||
|
// sonst setzt jeder Seitenaufruf zwei identische Anfragen ab.
|
||||||
|
const skipInitialRefetch = useRef(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (skipInitialRefetch.current) {
|
||||||
|
skipInitialRefetch.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (onRefetch) {
|
if (onRefetch) {
|
||||||
onRefetch(filters.type ? { type: filters.type } : {});
|
onRefetch(filters.type ? { type: filters.type } : {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
||||||
|
import { BASE_PATH } from '../../utils/constants';
|
||||||
import './PasswordReset.css';
|
import './PasswordReset.css';
|
||||||
|
|
||||||
|
// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst.
|
||||||
|
// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal.
|
||||||
|
const APP_HOME = `${BASE_PATH}/`;
|
||||||
|
|
||||||
|
// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…).
|
||||||
|
// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1
|
||||||
|
// mit leerem Token-Feld – der Mailversand war damit wirkungslos.
|
||||||
|
const tokenFromUrl = () => {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
try {
|
||||||
|
return new URLSearchParams(window.location.search).get('token') || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function PasswordReset() {
|
function PasswordReset() {
|
||||||
const [step, setStep] = useState('request'); // 'request' or 'reset'
|
const initialToken = tokenFromUrl();
|
||||||
|
const [step, setStep] = useState(initialToken ? 'reset' : 'request');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState(initialToken);
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -62,7 +80,7 @@ function PasswordReset() {
|
||||||
setToken('');
|
setToken('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
window.location.href = '/'; // Redirect to login
|
window.location.href = APP_HOME; // Redirect to login
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: result.message });
|
setMessage({ type: 'error', text: result.message });
|
||||||
|
|
@ -100,7 +118,7 @@ function PasswordReset() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<a href="/">Zurück zum Login</a>
|
<a href={APP_HOME}>Zurück zum Login</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useConfigContext } from '../../contexts/ConfigContext';
|
import { useConfigContext } from '../../contexts/ConfigContext';
|
||||||
import MapView from '../map/MapView';
|
import MapView from '../map/MapView';
|
||||||
import FilterPanel from '../users/FilterPanel';
|
import FilterPanel from '../users/FilterPanel';
|
||||||
|
|
@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers';
|
||||||
import { getGeocodeByPostalCode } from '../../services/users';
|
import { getGeocodeByPostalCode } from '../../services/users';
|
||||||
import './PublicUserList.css';
|
import './PublicUserList.css';
|
||||||
|
|
||||||
|
const LOCATION_KEY = 'userLocation';
|
||||||
|
|
||||||
|
// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt
|
||||||
|
// kann beschädigt sein. Beides darf die Seite nicht mitreißen.
|
||||||
|
const readStoredLocation = () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LOCATION_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PublicUserList = ({ users, loading, onRefetch }) => {
|
const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
const { userTypeLabels } = useConfigContext();
|
const { userTypeLabels } = useConfigContext();
|
||||||
|
|
||||||
// Load saved location from localStorage on mount
|
|
||||||
const savedLocation = localStorage.getItem('userLocation');
|
|
||||||
const initialCoords = savedLocation ? JSON.parse(savedLocation) : null;
|
|
||||||
|
|
||||||
const [showMap, setShowMap] = useState(true);
|
const [showMap, setShowMap] = useState(true);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle');
|
// Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein
|
||||||
|
// ungeschütztes JSON.parse im Render-Pfad: ein beschädigter Eintrag – oder ein
|
||||||
|
// Browser, der Site-Data blockiert – hat die gesamte Liste weiß werden lassen.
|
||||||
|
const [coords, setCoords] = useState(readStoredLocation);
|
||||||
|
const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle');
|
||||||
const [locationError, setLocationError] = useState('');
|
const [locationError, setLocationError] = useState('');
|
||||||
const [coords, setCoords] = useState(initialCoords);
|
|
||||||
const [radiusKm, setRadiusKm] = useState(100);
|
const [radiusKm, setRadiusKm] = useState(100);
|
||||||
const [postalCode, setPostalCode] = useState('');
|
const [postalCode, setPostalCode] = useState('');
|
||||||
const [postalSearching, setPostalSearching] = useState(false);
|
const [postalSearching, setPostalSearching] = useState(false);
|
||||||
|
|
@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
|
|
||||||
// Save location to localStorage whenever it changes
|
// Save location to localStorage whenever it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (coords) {
|
if (!coords) return;
|
||||||
localStorage.setItem('userLocation', JSON.stringify(coords));
|
try {
|
||||||
|
localStorage.setItem(LOCATION_KEY, JSON.stringify(coords));
|
||||||
|
} catch {
|
||||||
|
// Speicher nicht verfügbar – der Standort gilt dann nur für diese Sitzung.
|
||||||
}
|
}
|
||||||
}, [coords]);
|
}, [coords]);
|
||||||
|
|
||||||
|
|
@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
setFilters(prev => ({ ...prev, [key]: value }));
|
setFilters(prev => ({ ...prev, [key]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Re-fetch from backend when type filter changes (server-side filtering)
|
// Re-fetch from backend when type filter changes (server-side filtering).
|
||||||
|
// Der erste Lauf wird übersprungen: useUsers lädt beim Mounten bereits selbst,
|
||||||
|
// sonst setzt jeder Seitenaufruf zwei identische Anfragen ab.
|
||||||
|
const skipInitialRefetch = useRef(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (skipInitialRefetch.current) {
|
||||||
|
skipInitialRefetch.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (onRefetch) {
|
if (onRefetch) {
|
||||||
onRefetch(filters.type ? { type: filters.type } : {});
|
onRefetch(filters.type ? { type: filters.type } : {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
import { requestPasswordReset, resetPassword } from '../../services/passwordReset';
|
||||||
|
import { BASE_PATH } from '../../utils/constants';
|
||||||
import './PasswordReset.css';
|
import './PasswordReset.css';
|
||||||
|
|
||||||
|
// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst.
|
||||||
|
// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal.
|
||||||
|
const APP_HOME = `${BASE_PATH}/`;
|
||||||
|
|
||||||
|
// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…).
|
||||||
|
// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1
|
||||||
|
// mit leerem Token-Feld – der Mailversand war damit wirkungslos.
|
||||||
|
const tokenFromUrl = () => {
|
||||||
|
if (typeof window === 'undefined') return '';
|
||||||
|
try {
|
||||||
|
return new URLSearchParams(window.location.search).get('token') || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function PasswordReset() {
|
function PasswordReset() {
|
||||||
const [step, setStep] = useState('request'); // 'request' or 'reset'
|
const initialToken = tokenFromUrl();
|
||||||
|
const [step, setStep] = useState(initialToken ? 'reset' : 'request');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState(initialToken);
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -62,7 +80,7 @@ function PasswordReset() {
|
||||||
setToken('');
|
setToken('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
window.location.href = '/'; // Redirect to login
|
window.location.href = APP_HOME; // Redirect to login
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: result.message });
|
setMessage({ type: 'error', text: result.message });
|
||||||
|
|
@ -100,7 +118,7 @@ function PasswordReset() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<a href="/">Zurück zum Login</a>
|
<a href={APP_HOME}>Zurück zum Login</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useConfigContext } from '../../contexts/ConfigContext';
|
import { useConfigContext } from '../../contexts/ConfigContext';
|
||||||
import MapView from '../map/MapView';
|
import MapView from '../map/MapView';
|
||||||
import FilterPanel from '../users/FilterPanel';
|
import FilterPanel from '../users/FilterPanel';
|
||||||
|
|
@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers';
|
||||||
import { getGeocodeByPostalCode } from '../../services/users';
|
import { getGeocodeByPostalCode } from '../../services/users';
|
||||||
import './PublicUserList.css';
|
import './PublicUserList.css';
|
||||||
|
|
||||||
|
const LOCATION_KEY = 'userLocation';
|
||||||
|
|
||||||
|
// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt
|
||||||
|
// kann beschaedigt sein. Beides darf die Seite nicht mitreissen.
|
||||||
|
const readStoredLocation = () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LOCATION_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PublicUserList = ({ users, loading, onRefetch }) => {
|
const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
const { userTypeLabels } = useConfigContext();
|
const { userTypeLabels } = useConfigContext();
|
||||||
|
|
||||||
// Load saved location from localStorage on mount
|
|
||||||
const savedLocation = localStorage.getItem('userLocation');
|
|
||||||
const initialCoords = savedLocation ? JSON.parse(savedLocation) : null;
|
|
||||||
|
|
||||||
const [showMap, setShowMap] = useState(true);
|
const [showMap, setShowMap] = useState(true);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle');
|
// Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein
|
||||||
|
// ungeschuetztes JSON.parse im Render-Pfad: ein beschaedigter Eintrag - oder ein
|
||||||
|
// Browser, der Site-Data blockiert - hat die gesamte Liste weiss werden lassen.
|
||||||
|
const [coords, setCoords] = useState(readStoredLocation);
|
||||||
|
const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle');
|
||||||
const [locationError, setLocationError] = useState('');
|
const [locationError, setLocationError] = useState('');
|
||||||
const [coords, setCoords] = useState(initialCoords);
|
|
||||||
const [radiusKm, setRadiusKm] = useState(100);
|
const [radiusKm, setRadiusKm] = useState(100);
|
||||||
const [postalCode, setPostalCode] = useState('');
|
const [postalCode, setPostalCode] = useState('');
|
||||||
const [postalSearching, setPostalSearching] = useState(false);
|
const [postalSearching, setPostalSearching] = useState(false);
|
||||||
|
|
@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
|
|
||||||
// Save location to localStorage whenever it changes
|
// Save location to localStorage whenever it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (coords) {
|
if (!coords) return;
|
||||||
localStorage.setItem('userLocation', JSON.stringify(coords));
|
try {
|
||||||
|
localStorage.setItem(LOCATION_KEY, JSON.stringify(coords));
|
||||||
|
} catch {
|
||||||
|
// Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung.
|
||||||
}
|
}
|
||||||
}, [coords]);
|
}, [coords]);
|
||||||
|
|
||||||
|
|
@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => {
|
||||||
setFilters(prev => ({ ...prev, [key]: value }));
|
setFilters(prev => ({ ...prev, [key]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Re-fetch from backend when type filter changes (server-side filtering)
|
// Re-fetch from backend when type filter changes (server-side filtering).
|
||||||
|
// Der erste Lauf wird uebersprungen: useUsers laedt beim Mounten bereits selbst,
|
||||||
|
// sonst setzt jeder Seitenaufruf zwei identische Anfragen ab.
|
||||||
|
const skipInitialRefetch = useRef(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (skipInitialRefetch.current) {
|
||||||
|
skipInitialRefetch.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (onRefetch) {
|
if (onRefetch) {
|
||||||
onRefetch(filters.type ? { type: filters.type } : {});
|
onRefetch(filters.type ? { type: filters.type } : {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue