11"use client" ;
22
3- import React , { useEffect , useState } from 'react' ;
3+ import React , { useEffect , useState , useRef } from 'react' ;
44import { authAPI , pointsAPI } from '../lib/api' ;
55import { ThemeManager } from '../utils/themeManager' ;
66import '../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