Initial commit - Rentall App
- Full-stack rental marketplace application - React frontend with TypeScript - Node.js/Express backend with JWT authentication - Features: item listings, rental requests, calendar availability, user profiles
This commit is contained in:
159
frontend/src/components/AddressAutocomplete.tsx
Normal file
159
frontend/src/components/AddressAutocomplete.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface AddressSuggestion {
|
||||
place_id: string;
|
||||
display_name: string;
|
||||
lat: string;
|
||||
lon: string;
|
||||
}
|
||||
|
||||
interface AddressAutocompleteProps {
|
||||
value: string;
|
||||
onChange: (value: string, lat?: number, lon?: number) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Address",
|
||||
required = false,
|
||||
className = "form-control",
|
||||
id,
|
||||
name
|
||||
}) => {
|
||||
const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const debounceTimer = useRef<number | undefined>(undefined);
|
||||
|
||||
// Handle clicking outside to close suggestions
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchAddressSuggestions = async (query: string) => {
|
||||
if (query.length < 3) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Using Nominatim API (OpenStreetMap) for free geocoding
|
||||
// In production, you might want to use Google Places API or another service
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?` +
|
||||
`q=${encodeURIComponent(query)}&` +
|
||||
`format=json&` +
|
||||
`limit=5&` +
|
||||
`countrycodes=us`
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSuggestions(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching address suggestions:', error);
|
||||
setSuggestions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
onChange(newValue);
|
||||
setShowSuggestions(true);
|
||||
|
||||
// Debounce the API call
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
|
||||
debounceTimer.current = window.setTimeout(() => {
|
||||
fetchAddressSuggestions(newValue);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: AddressSuggestion) => {
|
||||
onChange(
|
||||
suggestion.display_name,
|
||||
parseFloat(suggestion.lat),
|
||||
parseFloat(suggestion.lon)
|
||||
);
|
||||
setShowSuggestions(false);
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className="position-relative">
|
||||
<input
|
||||
type="text"
|
||||
className={className}
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
{showSuggestions && (suggestions.length > 0 || loading) && (
|
||||
<div
|
||||
className="position-absolute w-100 bg-white border rounded-bottom shadow-sm"
|
||||
style={{ top: '100%', zIndex: 1000, maxHeight: '300px', overflowY: 'auto' }}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="p-2 text-center text-muted">
|
||||
<small>Searching addresses...</small>
|
||||
</div>
|
||||
) : (
|
||||
suggestions.map((suggestion) => (
|
||||
<div
|
||||
key={suggestion.place_id}
|
||||
className="p-2 border-bottom cursor-pointer"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
onMouseEnter={(e) => e.currentTarget.classList.add('bg-light')}
|
||||
onMouseLeave={(e) => e.currentTarget.classList.remove('bg-light')}
|
||||
>
|
||||
<small className="d-block text-truncate">
|
||||
{suggestion.display_name}
|
||||
</small>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressAutocomplete;
|
||||
Reference in New Issue
Block a user