Skip to content

Commit bb7c63f

Browse files
committed
added search for add points
1 parent 6ae18cc commit bb7c63f

File tree

1 file changed

+235
-3
lines changed

1 file changed

+235
-3
lines changed

frontend/src/components/AddPoint.jsx

Lines changed: 235 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useEffect, useState } from 'react';
3+
import React, { useEffect, useState, useRef } from 'react';
44
import { authAPI, pointsAPI } from '../lib/api';
55
import { ThemeManager } from '../utils/themeManager';
66
import '../styles/login.css'; // ← pull in the animated gradient & centering
@@ -18,6 +18,15 @@ export default function AddPoint() {
1818
const [addedPoint, setAddedPoint] = useState(null);
1919
const [referrerPage, setReferrerPage] = useState('map');
2020
const [theme, setTheme] = useState(() => ThemeManager.getTheme());
21+
22+
// New state for location search
23+
const [searchQuery, setSearchQuery] = useState('');
24+
const [searchResults, setSearchResults] = useState([]);
25+
const [searchLoading, setSearchLoading] = useState(false);
26+
const [selectedLocation, setSelectedLocation] = useState(null);
27+
const searchTimeoutRef = useRef(null);
28+
const [showSearchResults, setShowSearchResults] = useState(false);
29+
const searchResultsRef = useRef(null);
2130

2231
useEffect(() => {
2332
// Initialize theme
@@ -70,9 +79,92 @@ export default function AddPoint() {
7079
);
7180
}
7281

73-
return removeListener;
82+
// Add click outside listener for search results
83+
const handleClickOutside = (event) => {
84+
if (searchResultsRef.current && !searchResultsRef.current.contains(event.target)) {
85+
setShowSearchResults(false);
86+
}
87+
};
88+
89+
document.addEventListener('mousedown', handleClickOutside);
90+
91+
return () => {
92+
removeListener();
93+
document.removeEventListener('mousedown', handleClickOutside);
94+
};
7495
}, []);
7596

97+
// Search for locations based on query
98+
const searchLocations = async (query) => {
99+
if (!query || query.trim().length < 3) {
100+
setSearchResults([]);
101+
return;
102+
}
103+
104+
setSearchLoading(true);
105+
try {
106+
// Using OpenStreetMap Nominatim API for geocoding
107+
// Added countrycodes=ca to limit results to Canada
108+
const response = await fetch(
109+
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&countrycodes=ca&limit=5`
110+
);
111+
112+
if (!response.ok) {
113+
throw new Error('Search request failed');
114+
}
115+
116+
const data = await response.json();
117+
setSearchResults(data);
118+
setShowSearchResults(true);
119+
} catch (err) {
120+
console.error('Location search error:', err);
121+
setError('Location search failed. Please try again or enter coordinates manually.');
122+
} finally {
123+
setSearchLoading(false);
124+
}
125+
};
126+
127+
// Handle search input change with debounce
128+
const handleSearchInputChange = (e) => {
129+
const value = e.target.value;
130+
setSearchQuery(value);
131+
132+
// Clear any existing timeout
133+
if (searchTimeoutRef.current) {
134+
clearTimeout(searchTimeoutRef.current);
135+
}
136+
137+
// Set a new timeout to debounce the search
138+
searchTimeoutRef.current = setTimeout(() => {
139+
searchLocations(value);
140+
}, 500); // 500ms debounce time
141+
};
142+
143+
const [locationSelected, setLocationSelected] = useState(false);
144+
145+
// Then modify the handleLocationSelect function
146+
const handleLocationSelect = (location) => {
147+
setSelectedLocation(location);
148+
149+
// Make sure we're parsing the string values to numbers and then formatting them
150+
const newLat = parseFloat(location.lat).toFixed(6);
151+
const newLon = parseFloat(location.lon).toFixed(6);
152+
153+
// Force update the coordinate fields
154+
setLat(newLat);
155+
setLon(newLon);
156+
157+
// Update the search query text
158+
setSearchQuery(location.display_name);
159+
setShowSearchResults(false);
160+
161+
// Show visual confirmation
162+
setLocationSelected(true);
163+
164+
// Reset the confirmation after 2 seconds
165+
setTimeout(() => setLocationSelected(false), 2000);
166+
};
167+
76168
const handleSubmit = async (e) => {
77169
e.preventDefault();
78170
setError('');
@@ -286,7 +378,7 @@ export default function AddPoint() {
286378
<div className="loginpage">
287379
<div className="login-container">
288380
<div className="login-form-container">
289-
{/* your existing AddPoint card */}
381+
{/* your existing AddPoint "card" */}
290382
<div style={{
291383
maxWidth: '500px',
292384
width: '100%',
@@ -329,6 +421,127 @@ export default function AddPoint() {
329421

330422
{/* Form */}
331423
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
424+
{/* New Location Search Field */}
425+
<div style={{ position: 'relative' }}>
426+
<label htmlFor="location-search"
427+
style={{
428+
display: 'block',
429+
fontSize: '14px',
430+
fontWeight: '600',
431+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
432+
marginBottom: '6px'
433+
}}
434+
>
435+
🔍 Search Location
436+
</label>
437+
<input
438+
id="location-search"
439+
type="text"
440+
value={searchQuery}
441+
onChange={handleSearchInputChange}
442+
placeholder="Enter an address or place name"
443+
style={{
444+
width: '100%',
445+
padding: '12px 16px',
446+
border: theme === 'light' ? '1px solid rgba(0, 0, 0, 0.1)' : '1px solid rgba(255, 255, 255, 0.2)',
447+
borderRadius: '12px',
448+
fontSize: '16px',
449+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
450+
backgroundColor: theme === 'light' ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.1)',
451+
transition: 'all 0.2s ease',
452+
boxSizing: 'border-box',
453+
backdropFilter: 'blur(10px)',
454+
outline: 'none',
455+
paddingRight: searchLoading ? '40px' : '16px'
456+
}}
457+
onFocus={(e) => {
458+
e.target.style.backgroundColor = theme === 'light' ? 'rgba(255, 255, 255, 1)' : 'rgba(255, 255, 255, 0.15)';
459+
if (searchResults.length > 0) {
460+
setShowSearchResults(true);
461+
}
462+
}}
463+
onBlur={(e) => {
464+
e.target.style.backgroundColor = theme === 'light' ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.1)';
465+
// Don't hide results immediately to allow for clicking on them
466+
// setTimeout(() => setShowSearchResults(false), 200);
467+
}}
468+
/>
469+
{searchLoading && (
470+
<div style={{
471+
position: 'absolute',
472+
right: '12px',
473+
top: '50%',
474+
transform: 'translateY(-50%)',
475+
width: '20px',
476+
height: '20px',
477+
border: `2px solid ${theme === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}`,
478+
borderTop: `2px solid ${theme === 'dark' ? 'white' : 'black'}`,
479+
borderRadius: '50%',
480+
animation: 'spin 1s linear infinite'
481+
}}></div>
482+
)}
483+
484+
{/* Search Results Dropdown */}
485+
{showSearchResults && searchResults.length > 0 && (
486+
<div
487+
ref={searchResultsRef}
488+
style={{
489+
position: 'absolute',
490+
top: '100%',
491+
left: 0,
492+
width: '100%',
493+
maxHeight: '250px',
494+
overflowY: 'auto',
495+
backgroundColor: theme === 'light' ? 'rgba(255, 255, 255, 0.95)' : 'rgba(40, 40, 40, 0.95)',
496+
border: theme === 'light' ? '1px solid rgba(0, 0, 0, 0.1)' : '1px solid rgba(255, 255, 255, 0.2)',
497+
borderRadius: '12px',
498+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)',
499+
zIndex: 10,
500+
marginTop: '5px',
501+
backdropFilter: 'blur(10px)'
502+
}}
503+
>
504+
{searchResults.map((result, index) => (
505+
<div
506+
key={index}
507+
onClick={() => handleLocationSelect(result)}
508+
style={{
509+
padding: '12px 16px',
510+
borderBottom: index < searchResults.length - 1
511+
? (theme === 'light' ? '1px solid rgba(0, 0, 0, 0.05)' : '1px solid rgba(255, 255, 255, 0.1)')
512+
: 'none',
513+
cursor: 'pointer',
514+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
515+
transition: 'background-color 0.2s ease',
516+
fontSize: '14px'
517+
}}
518+
onMouseEnter={(e) => {
519+
e.currentTarget.style.backgroundColor = theme === 'light'
520+
? 'rgba(0, 0, 0, 0.05)'
521+
: 'rgba(255, 255, 255, 0.1)';
522+
}}
523+
onMouseLeave={(e) => {
524+
e.currentTarget.style.backgroundColor = 'transparent';
525+
}}
526+
>
527+
<div style={{ fontWeight: '600', marginBottom: '2px' }}>
528+
{result.display_name.split(',')[0]}
529+
</div>
530+
<div style={{
531+
fontSize: '12px',
532+
opacity: 0.7,
533+
whiteSpace: 'nowrap',
534+
overflow: 'hidden',
535+
textOverflow: 'ellipsis'
536+
}}>
537+
{result.display_name}
538+
</div>
539+
</div>
540+
))}
541+
</div>
542+
)}
543+
</div>
544+
332545
<div>
333546
<label htmlFor="latitude"
334547
style={{
@@ -571,6 +784,25 @@ export default function AddPoint() {
571784
{error}
572785
</div>
573786
)}
787+
788+
{/* Location Selected Message */}
789+
{locationSelected && (
790+
<div style={{
791+
marginTop: '5px',
792+
padding: '8px 12px',
793+
backgroundColor: 'rgba(52, 199, 89, 0.1)',
794+
border: '1px solid rgba(52, 199, 89, 0.2)',
795+
borderRadius: '8px',
796+
color: '#34c759',
797+
fontSize: '13px',
798+
fontWeight: '500',
799+
display: 'flex',
800+
alignItems: 'center',
801+
gap: '5px'
802+
}}>
803+
<span></span> Location selected and coordinates updated
804+
</div>
805+
)}
574806
</div>
575807
</div>
576808
</div>

0 commit comments

Comments
 (0)