diff --git a/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js b/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js +++ b/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; 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() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js index 5a74055..446f533 100644 --- a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js +++ b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js @@ -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 MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; 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 { 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 [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 [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { 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(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); } diff --git a/nachsuche/frontend/src/components/auth/PasswordReset.js b/nachsuche/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/nachsuche/frontend/src/components/auth/PasswordReset.js +++ b/nachsuche/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; 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() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/nachsuche/frontend/src/components/public/PublicUserList.js b/nachsuche/frontend/src/components/public/PublicUserList.js index 9148e67..59b4bd8 100644 --- a/nachsuche/frontend/src/components/public/PublicUserList.js +++ b/nachsuche/frontend/src/components/public/PublicUserList.js @@ -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 MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; 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 { 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 [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 [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfügbar – der Standort gilt dann nur für diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { 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(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); } diff --git a/stoeberhunde/frontend/src/components/auth/PasswordReset.js b/stoeberhunde/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/stoeberhunde/frontend/src/components/auth/PasswordReset.js +++ b/stoeberhunde/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; 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() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/stoeberhunde/frontend/src/components/public/PublicUserList.js b/stoeberhunde/frontend/src/components/public/PublicUserList.js index 3773b70..b3df037 100644 --- a/stoeberhunde/frontend/src/components/public/PublicUserList.js +++ b/stoeberhunde/frontend/src/components/public/PublicUserList.js @@ -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 MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; 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 { 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 [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 [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { 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(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); }