Skip to content

Commit 613cc0b

Browse files
committed
Improvements: Removed manual location setting in the Header page, fixed the bug for consistent location tracking and opening of map UI, applied the map UI to SP header
1 parent a8427ad commit 613cc0b

File tree

5 files changed

+251
-102
lines changed

5 files changed

+251
-102
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Add service-level certificate verification system with PDF and image certificate uploads replacing user-level verification
1919
- Add frontend service image management hooks and utilities with support for galleries
2020
- Integrate image processing utilities in service creation workflow with support for optional image uploads
21+
22+
### Fixed
23+
24+
- Fix location persistence across browser sessions and page reloads in client header
25+
- Fix location state synchronization between context and component in header component
2126
- Add immediate UI feedback for service image upload and removal operations with proper Save/Cancel workflow
2227

2328
### Changed

src/frontend/src/components/client/Header.tsx

Lines changed: 122 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// --- Imports ---
2-
import React, { useState, useEffect } from "react";
2+
import React, { useState, useEffect, useCallback } from "react";
33
import { MapPinIcon, UserCircleIcon } from "@heroicons/react/24/solid";
44
import { Link, useNavigate } from "react-router-dom";
55
import { useAuth } from "../../context/AuthContext";
@@ -26,15 +26,15 @@ L.Icon.Default.mergeOptions({
2626
});
2727

2828
// --- Main Header Component ---
29-
const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
29+
const Header: React.FC<HeaderProps> = ({ className }) => {
3030
const navigate = useNavigate();
31-
const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
32-
33-
// --- State: Geolocation for map modal ---
34-
const [geoLocation, setGeoLocation] = useState<{
35-
latitude: number;
36-
longitude: number;
37-
} | null>(null);
31+
const {
32+
isAuthenticated,
33+
isLoading: isAuthLoading,
34+
location,
35+
setLocation,
36+
locationStatus,
37+
} = useAuth();
3838

3939
// --- State: User profile ---
4040
const [profile, setProfile] = useState<any>(null);
@@ -66,30 +66,8 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
6666
// --- State: Show/hide map modal ---
6767
const [showMap, setShowMap] = useState(false);
6868

69-
// Effect: fetch user profile and location info after auth
70-
// --- Effect: Fetch user profile and detect location on mount ---
69+
// --- Effect: Fetch user profile and update location address ---
7170
useEffect(() => {
72-
// Helper: retry fetch with delay (for OpenStreetMap reverse geocoding)
73-
const fetchWithRetry = async (
74-
url: string,
75-
attempts: number,
76-
delayMs: number,
77-
): Promise<any> => {
78-
for (let i = 0; i < attempts; i++) {
79-
try {
80-
const res = await fetch(url);
81-
if (!res.ok) throw new Error("Fetch failed");
82-
return await res.json();
83-
} catch (err) {
84-
if (i < attempts - 1) {
85-
await new Promise((resolve) => setTimeout(resolve, delayMs));
86-
}
87-
}
88-
}
89-
throw new Error("All fetch attempts failed");
90-
};
91-
92-
// Main: load profile and location
9371
const loadInitialData = async () => {
9472
// Fetch user profile if authenticated
9573
if (isAuthenticated) {
@@ -100,60 +78,108 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
10078
/* Profile fetch failed */
10179
}
10280
}
81+
82+
// Handle location address display
10383
setLocationLoading(true);
104-
// Request geolocation from browser
105-
if (navigator.geolocation) {
106-
navigator.geolocation.getCurrentPosition(
107-
async (position) => {
108-
const { latitude, longitude } = position.coords;
109-
setGeoLocation({ latitude, longitude });
110-
try {
111-
const data = await fetchWithRetry(
112-
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`,
113-
3, // attempts
114-
1500, // delay ms
115-
);
116-
const province =
117-
data.address.county ||
118-
data.address.state ||
119-
data.address.region ||
120-
data.address.province ||
121-
"";
122-
const municipality =
123-
data.address.city ||
124-
data.address.town ||
125-
data.address.village ||
126-
"";
127-
setUserProvince(province);
128-
setUserAddress(municipality);
129-
setLocationLoading(false);
130-
} catch (err) {
131-
setUserAddress("Could not determine address");
132-
setUserProvince("");
133-
setLocationLoading(false);
134-
}
135-
},
136-
() => {
137-
// Permission denied or error
84+
if (locationStatus === "allowed" && location) {
85+
// Check if we have cached address data
86+
const cachedAddress = localStorage.getItem(
87+
`address_${location.latitude}_${location.longitude}`,
88+
);
89+
if (cachedAddress) {
90+
try {
91+
const { address, province } = JSON.parse(cachedAddress);
92+
setUserAddress(address);
93+
setUserProvince(province);
13894
setLocationLoading(false);
139-
setUserAddress("");
95+
return;
96+
} catch {
97+
// Cache is corrupted, continue with API call
98+
}
99+
}
100+
101+
// Fetch address from API
102+
try {
103+
const res = await fetch(
104+
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${location.latitude}&lon=${location.longitude}`,
105+
);
106+
const data = await res.json();
107+
if (data && data.address) {
108+
const { road, suburb, city, town, village, county, state } =
109+
data.address;
110+
const province =
111+
county ||
112+
state ||
113+
data.address.region ||
114+
data.address.province ||
115+
"";
116+
const streetPart = road || "";
117+
const areaPart = suburb || village || "";
118+
const cityPart = city || town || "";
119+
const fullAddress = [streetPart, areaPart, cityPart]
120+
.filter(Boolean)
121+
.join(", ");
122+
const finalAddress = fullAddress || "Could not determine address";
123+
124+
setUserAddress(finalAddress);
125+
setUserProvince(province);
126+
127+
// Cache the address for faster subsequent loads
128+
localStorage.setItem(
129+
`address_${location.latitude}_${location.longitude}`,
130+
JSON.stringify({ address: finalAddress, province }),
131+
);
132+
} else {
133+
setUserAddress("Could not determine address");
140134
setUserProvince("");
141-
setGeoLocation(null);
142-
},
143-
);
135+
}
136+
} catch (error) {
137+
setUserAddress("Could not determine address");
138+
setUserProvince("");
139+
} finally {
140+
setLocationLoading(false);
141+
}
144142
} else {
143+
// Handle cases where location is not yet known or denied
145144
setLocationLoading(false);
146-
setUserAddress("");
145+
switch (locationStatus) {
146+
case "denied":
147+
setUserAddress("Location not shared");
148+
break;
149+
case "not_set":
150+
case "unsupported":
151+
default:
152+
setUserAddress("Location not set");
153+
break;
154+
}
147155
setUserProvince("");
148-
setGeoLocation(null);
149156
}
150157
};
158+
151159
if (!isAuthLoading) {
152160
loadInitialData();
153161
}
154-
// Only run once after auth loads
155-
// eslint-disable-next-line react-hooks/exhaustive-deps
156-
}, [isAuthenticated, isAuthLoading]);
162+
}, [isAuthenticated, isAuthLoading, location, locationStatus]);
163+
const handleRequestLocation = useCallback(() => {
164+
setLocationLoading(true);
165+
setUserAddress("Detecting location...");
166+
if (navigator.geolocation) {
167+
navigator.geolocation.getCurrentPosition(
168+
(position) => {
169+
const { latitude, longitude } = position.coords;
170+
setLocation("allowed", { latitude, longitude });
171+
},
172+
(error) => {
173+
console.error("Error getting location:", error);
174+
setLocation("denied");
175+
setLocationLoading(false);
176+
},
177+
);
178+
} else {
179+
setLocation("unsupported");
180+
setLocationLoading(false);
181+
}
182+
}, [setLocation]);
157183

158184
// --- Effect: Randomize search bar placeholder after location loads ---
159185
useEffect(() => {
@@ -176,14 +202,14 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
176202

177203
// --- Map Modal: Shows user's detected location on a map ---
178204
const MapModal: React.FC = () => {
179-
if (!geoLocation || !geoLocation.latitude || !geoLocation.longitude)
180-
return null;
181-
// Close modal if background is clicked
205+
if (!location || !location.latitude || !location.longitude) return null;
206+
182207
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
183208
if (e.target === e.currentTarget) {
184209
setShowMap(false);
185210
}
186211
};
212+
187213
return (
188214
<div
189215
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
@@ -203,7 +229,7 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
203229
</button>
204230
<div className="flex-1 overflow-hidden rounded-b-lg">
205231
<MapContainer
206-
center={[geoLocation.latitude, geoLocation.longitude]}
232+
center={[location.latitude, location.longitude]}
207233
zoom={16}
208234
scrollWheelZoom={true}
209235
style={{ height: "100%", width: "100%" }}
@@ -212,7 +238,7 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
212238
attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
213239
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
214240
/>
215-
<Marker position={[geoLocation.latitude, geoLocation.longitude]}>
241+
<Marker position={[location.latitude, location.longitude]}>
216242
<Popup>You are here</Popup>
217243
</Marker>
218244
</MapContainer>
@@ -301,7 +327,10 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
301327
<span className="animate-pulse text-gray-500">
302328
Detecting location...
303329
</span>
304-
) : userAddress && userProvince ? (
330+
) : locationStatus === "allowed" &&
331+
location &&
332+
userAddress &&
333+
userProvince ? (
305334
<button
306335
type="button"
307336
className="text-left font-medium text-blue-900 transition-all duration-200 hover:text-lg hover:text-blue-700 focus:outline-none"
@@ -310,17 +339,22 @@ const Header: React.FC<HeaderProps> = ({ className, manualLocation }) => {
310339
>
311340
{userAddress}, {userProvince}
312341
</button>
313-
) : manualLocation &&
314-
manualLocation.municipality &&
315-
manualLocation.province ? (
316-
<span className="text-left font-medium text-blue-900">
317-
{manualLocation.municipality}, {manualLocation.province}
318-
</span>
319342
) : (
320-
<span className="text-left text-gray-500">Location not set</span>
343+
<span className="text-gray-600">{userAddress}</span>
321344
)}
322345
</div>
323346
</div>
347+
{!locationLoading &&
348+
(locationStatus === "denied" ||
349+
locationStatus === "not_set" ||
350+
locationStatus === "unsupported") && (
351+
<button
352+
onClick={handleRequestLocation}
353+
className="mt-2 w-full rounded-lg bg-yellow-300 p-2 text-center text-sm font-semibold text-blue-700 transition-colors hover:bg-yellow-400"
354+
>
355+
Share Location
356+
</button>
357+
)}
324358
{/* --- Search Bar for Service Queries --- */}
325359
<form
326360
className="mt-4 w-full"

0 commit comments

Comments
 (0)