BIG WEB UPDATE
This commit is contained in:
@@ -9,18 +9,44 @@ interface Props {
|
||||
value?: string;
|
||||
onChange?: (v: string) => void;
|
||||
onSelect: (feature: any) => void;
|
||||
onMapPick?: () => void; // New prop for map picking mode
|
||||
isMapPickingMode?: boolean; // Whether currently in map picking mode
|
||||
}
|
||||
|
||||
export default function GeocodeInput({ mapRef, placeholder = 'Search', value = '', onChange, onSelect }: Props) {
|
||||
export default function GeocodeInput({
|
||||
mapRef,
|
||||
placeholder = 'Search location or enter coordinates...',
|
||||
value = '',
|
||||
onChange,
|
||||
onSelect,
|
||||
onMapPick,
|
||||
isMapPickingMode = false
|
||||
}: Props) {
|
||||
const [query, setQuery] = useState<string>(value);
|
||||
const [suggestions, setSuggestions] = useState<any[]>([]);
|
||||
const [showDropdown, setShowDropdown] = useState<boolean>(false);
|
||||
const timer = useRef<number | null>(null);
|
||||
const mounted = useRef(true);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => { mounted.current = false; };
|
||||
}, []);
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== query) setQuery(value);
|
||||
}, [value]);
|
||||
@@ -29,12 +55,81 @@ export default function GeocodeInput({ mapRef, placeholder = 'Search', value = '
|
||||
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || mapboxgl.accessToken || undefined;
|
||||
if (!token) return [];
|
||||
if (!q || q.trim().length === 0) return [];
|
||||
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(q)}.json?autocomplete=true&limit=6&types=place,locality,address,region,poi&access_token=${token}`;
|
||||
|
||||
// Check if the query looks like coordinates (lat,lng or lng,lat)
|
||||
const coordinatePattern = /^(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)$/;
|
||||
const coordMatch = q.trim().match(coordinatePattern);
|
||||
|
||||
if (coordMatch) {
|
||||
const [, first, second] = coordMatch;
|
||||
const num1 = parseFloat(first);
|
||||
const num2 = parseFloat(second);
|
||||
|
||||
// Determine which is lat and which is lng based on typical ranges
|
||||
// Latitude: -90 to 90, Longitude: -180 to 180
|
||||
// For DC area: lat around 38-39, lng around -77
|
||||
let lat, lng;
|
||||
|
||||
if (Math.abs(num1) <= 90 && Math.abs(num2) <= 180) {
|
||||
// Check if first number looks like latitude for DC area
|
||||
if (num1 >= 38 && num1 <= 39 && num2 >= -78 && num2 <= -76) {
|
||||
lat = num1;
|
||||
lng = num2;
|
||||
} else if (num2 >= 38 && num2 <= 39 && num1 >= -78 && num1 <= -76) {
|
||||
lat = num2;
|
||||
lng = num1;
|
||||
} else {
|
||||
// Default assumption: first is lat, second is lng
|
||||
lat = num1;
|
||||
lng = num2;
|
||||
}
|
||||
|
||||
// Validate coordinates are in reasonable ranges
|
||||
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
||||
// Create a synthetic feature for coordinates
|
||||
return [{
|
||||
center: [lng, lat],
|
||||
place_name: `${lat}, ${lng}`,
|
||||
text: `${lat}, ${lng}`,
|
||||
properties: {
|
||||
isCoordinate: true
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat]
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Washington DC area bounding box: SW corner (-77.25, 38.80), NE corner (-76.90, 39.05)
|
||||
const dcBounds = '-77.25,38.80,-76.90,39.05';
|
||||
|
||||
// Add proximity to center of DC for better ranking
|
||||
const dcCenter = '-77.0369,38.9072'; // Washington DC coordinates
|
||||
|
||||
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(q)}.json?` +
|
||||
`autocomplete=true&limit=6&types=place,locality,address,region,poi&` +
|
||||
`bbox=${dcBounds}&proximity=${dcCenter}&` +
|
||||
`country=US&access_token=${token}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.features || [];
|
||||
|
||||
// Additional client-side filtering to ensure results are in DC area
|
||||
const dcAreaFeatures = (data.features || []).filter((feature: any) => {
|
||||
const coords = feature.center;
|
||||
if (!coords || coords.length !== 2) return false;
|
||||
|
||||
const [lng, lat] = coords;
|
||||
// Check if coordinates are within DC metropolitan area bounds
|
||||
return lng >= -77.25 && lng <= -76.90 && lat >= 38.80 && lat <= 39.05;
|
||||
});
|
||||
|
||||
return dcAreaFeatures;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
@@ -42,33 +137,105 @@ export default function GeocodeInput({ mapRef, placeholder = 'Search', value = '
|
||||
|
||||
useEffect(() => {
|
||||
if (timer.current) window.clearTimeout(timer.current);
|
||||
if (!query) { setSuggestions([]); return; }
|
||||
if (!query) {
|
||||
setSuggestions([]);
|
||||
setShowDropdown(false);
|
||||
return;
|
||||
}
|
||||
timer.current = window.setTimeout(async () => {
|
||||
const feats = await fetchSuggestions(query);
|
||||
if (mounted.current) setSuggestions(feats);
|
||||
if (mounted.current) {
|
||||
setSuggestions(feats);
|
||||
setShowDropdown(feats.length > 0);
|
||||
}
|
||||
}, 250) as unknown as number;
|
||||
return () => { if (timer.current) window.clearTimeout(timer.current); };
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-transparent text-white placeholder-gray-400 rounded-md"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => { setQuery(e.target.value); onChange && onChange(e.target.value); }}
|
||||
/>
|
||||
{suggestions.length > 0 && (
|
||||
<div className="absolute left-0 right-0 mt-1 bg-[#0b0b0c] border border-black/20 rounded-md overflow-hidden custom-suggestions">
|
||||
<div className="relative" ref={containerRef}>
|
||||
{/* Search bar container matching the design */}
|
||||
<div className="flex items-center bg-[#2a2a2a] border border-[#404040] rounded-lg overflow-hidden">
|
||||
{/* Input field */}
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-transparent text-[#f5f5f5] placeholder-[#9ca3af] py-3 px-4 focus:outline-none"
|
||||
placeholder={isMapPickingMode ? "Click on map to select location..." : placeholder}
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (!isMapPickingMode && suggestions.length > 0) {
|
||||
setShowDropdown(true);
|
||||
}
|
||||
}}
|
||||
disabled={isMapPickingMode}
|
||||
/>
|
||||
|
||||
{/* Pin button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onMapPick) {
|
||||
onMapPick();
|
||||
}
|
||||
}}
|
||||
className="px-4 py-3 bg-[#f5f5f5] text-[#1f2937] hover:bg-[#e5e7eb] focus:outline-none focus:ring-2 focus:ring-[#9ca3af] focus:ring-offset-2 focus:ring-offset-[#2a2a2a] transition-colors"
|
||||
title={isMapPickingMode ? "Cancel map picking" : "Pick point on map"}
|
||||
>
|
||||
{isMapPickingMode ? (
|
||||
// X icon for cancel
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
// Pin icon
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Suggestions dropdown */}
|
||||
{!isMapPickingMode && showDropdown && suggestions.length > 0 && (
|
||||
<div className="absolute left-0 right-0 mt-1 bg-[#2a2a2a] border border-[#404040] rounded-lg shadow-lg overflow-hidden z-50 max-h-64 overflow-y-auto">
|
||||
{suggestions.map((f: any, i: number) => (
|
||||
<button key={f.id || i} className="w-full text-left px-3 py-2 hover:bg-white/5" onClick={() => { onSelect(f); setSuggestions([]); }}>
|
||||
<div className="font-medium">{f.text}</div>
|
||||
{f.place_name && <div className="text-xs text-gray-400">{f.place_name.replace(f.text, '').replace(/^,\s*/, '')}</div>}
|
||||
<button
|
||||
key={f.id || i}
|
||||
className="w-full text-left px-4 py-3 hover:bg-[#3a3a3a] border-b border-[#404040] last:border-b-0"
|
||||
onClick={() => {
|
||||
onSelect(f);
|
||||
setSuggestions([]);
|
||||
setShowDropdown(false);
|
||||
setQuery(f.place_name || f.text);
|
||||
}}
|
||||
>
|
||||
<div className="font-medium text-[#f5f5f5]">{f.text}</div>
|
||||
{f.place_name && (
|
||||
<div className="text-xs text-[#9ca3af] mt-1">
|
||||
{f.place_name.replace(f.text, '').replace(/^,\s*/, '')}
|
||||
{!f.place_name.toLowerCase().includes('washington') && !f.place_name.toLowerCase().includes('dc') &&
|
||||
<span className="ml-1 text-[#60a5fa]">• Washington DC Area</span>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map picking mode indicator */}
|
||||
{isMapPickingMode && (
|
||||
<div className="absolute left-0 right-0 mt-1 bg-[#065f46] border border-[#10b981] rounded-lg p-3 z-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-[#10b981] rounded-full animate-pulse"></div>
|
||||
<span className="text-sm text-[#ecfdf5]">Click anywhere on the map to select a location</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user