diff --git a/frontend/src/components/FilterPanel.tsx b/frontend/src/components/FilterPanel.tsx new file mode 100644 index 0000000..e5fd739 --- /dev/null +++ b/frontend/src/components/FilterPanel.tsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect, useRef } from "react"; +import AddressAutocomplete from "./AddressAutocomplete"; +import { PlaceDetails } from "../services/placesService"; + +interface FilterPanelProps { + show: boolean; + onClose: () => void; + currentFilters: { + lat: string; + lng: string; + radius: string; + locationName?: string; + }; + onApplyFilters: (filters: { + lat: string; + lng: string; + radius: string; + locationName: string; + }) => void; +} + +const RADIUS_OPTIONS = [ + { value: "5", label: "5 miles" }, + { value: "10", label: "10 miles" }, + { value: "25", label: "25 miles" }, + { value: "50", label: "50 miles" }, + { value: "100", label: "100 miles" }, +]; + +const FilterPanel: React.FC = ({ + show, + onClose, + currentFilters, + onApplyFilters, +}) => { + const [locationInput, setLocationInput] = useState(""); + const [selectedLocation, setSelectedLocation] = useState<{ + lat: number; + lng: number; + name: string; + } | null>(null); + const [radius, setRadius] = useState(currentFilters.radius || "25"); + const panelRef = useRef(null); + + // Initialize from current filters + useEffect(() => { + if (currentFilters.locationName) { + setLocationInput(currentFilters.locationName); + if (currentFilters.lat && currentFilters.lng) { + setSelectedLocation({ + lat: parseFloat(currentFilters.lat), + lng: parseFloat(currentFilters.lng), + name: currentFilters.locationName, + }); + } + } + setRadius(currentFilters.radius || "25"); + }, [currentFilters]); + + // Close on click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + panelRef.current && + !panelRef.current.contains(event.target as Node) + ) { + // Check if click is on the filter button (parent handles this) + const target = event.target as HTMLElement; + if (!target.closest("[data-filter-button]")) { + onClose(); + } + } + }; + + if (show) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [show, onClose]); + + const handlePlaceSelect = (place: PlaceDetails) => { + if (place.geometry?.latitude && place.geometry?.longitude) { + const locationName = place.formattedAddress || locationInput; + setSelectedLocation({ + lat: place.geometry.latitude, + lng: place.geometry.longitude, + name: locationName, + }); + setLocationInput(locationName); + } + }; + + const handleApply = () => { + if (selectedLocation) { + onApplyFilters({ + lat: selectedLocation.lat.toString(), + lng: selectedLocation.lng.toString(), + radius, + locationName: selectedLocation.name, + }); + } + onClose(); + }; + + const handleClear = () => { + setLocationInput(""); + setSelectedLocation(null); + setRadius("25"); + onApplyFilters({ + lat: "", + lng: "", + radius: "", + locationName: "", + }); + onClose(); + }; + + if (!show) return null; + + return ( + <> + + +
+
+
+ + + {selectedLocation && ( +
+ + {selectedLocation.name} +
+ )} +
+ +
+ + +
+
+ +
+ + +
+
+ + ); +}; + +export default FilterPanel; diff --git a/frontend/src/components/LocationPromptModal.tsx b/frontend/src/components/LocationPromptModal.tsx deleted file mode 100644 index 8820d33..0000000 --- a/frontend/src/components/LocationPromptModal.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React, { useState } from "react"; -import { mapsAPI } from "../services/api"; - -interface LocationPromptModalProps { - show: boolean; - onClose: () => void; - onLocationSelect: (location: { lat: number; lng: number }) => void; -} - -const LocationPromptModal: React.FC = ({ - show, - onClose, - onLocationSelect, -}) => { - const [manualLocation, setManualLocation] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - if (!show) return null; - - const handleUseMyLocation = async () => { - setLoading(true); - setError(null); - - try { - const position = await new Promise( - (resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, { - enableHighAccuracy: false, - timeout: 10000, - maximumAge: 300000, // Cache for 5 minutes - }); - } - ); - - onLocationSelect({ - lat: position.coords.latitude, - lng: position.coords.longitude, - }); - } catch (err: any) { - if (err.code === 1) { - setError("Location access denied. Please enter your city or ZIP code."); - } else if (err.code === 2) { - setError("Location unavailable. Please enter your city or ZIP code."); - } else if (err.code === 3) { - setError("Location request timed out. Please enter your city or ZIP code."); - } else { - setError("Could not get location. Please enter manually."); - } - } finally { - setLoading(false); - } - }; - - const handleManualSubmit = async () => { - const trimmed = manualLocation.trim(); - if (!trimmed) return; - - setLoading(true); - setError(null); - - try { - // Geocode the input (works for both ZIP codes and city names) - const response = await mapsAPI.geocode({ - address: trimmed, - componentRestrictions: { country: "US" }, - }); - - const { latitude, longitude } = response.data; - - if (latitude && longitude) { - onLocationSelect({ lat: latitude, lng: longitude }); - } else { - setError("Could not find that location. Please try a different city or ZIP code."); - } - } catch (err: any) { - setError("Could not find that location. Please try a different city or ZIP code."); - } finally { - setLoading(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleManualSubmit(); - } - }; - - return ( -
-
-
-
-
- - Where are you looking? -
- -
-
-

- Help us show you items nearby -

- - - - {error && ( -
- - {error} -
- )} - -
-
- or -
-
- -
- setManualLocation(e.target.value)} - onKeyDown={handleKeyDown} - disabled={loading} - /> -
-
-
- - -
-
-
-
- ); -}; - -export default LocationPromptModal; diff --git a/frontend/src/pages/ItemList.tsx b/frontend/src/pages/ItemList.tsx index f32a05c..c850031 100644 --- a/frontend/src/pages/ItemList.tsx +++ b/frontend/src/pages/ItemList.tsx @@ -4,7 +4,7 @@ import { Item } from "../types"; import { itemAPI } from "../services/api"; import ItemCard from "../components/ItemCard"; import SearchResultsMap from "../components/SearchResultsMap"; -import LocationPromptModal from "../components/LocationPromptModal"; +import FilterPanel from "../components/FilterPanel"; import { useAuth } from "../contexts/AuthContext"; const ItemList: React.FC = () => { @@ -15,8 +15,10 @@ const ItemList: React.FC = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState<'list' | 'map'>('list'); - const [showLocationPrompt, setShowLocationPrompt] = useState(false); + const [showFilterPanel, setShowFilterPanel] = useState(false); + const [locationName, setLocationName] = useState(searchParams.get("locationName") || ""); const locationCheckDone = useRef(false); + const filterButtonRef = useRef(null); const [filters, setFilters] = useState({ search: searchParams.get("search") || "", city: searchParams.get("city") || "", @@ -26,13 +28,12 @@ const ItemList: React.FC = () => { radius: searchParams.get("radius") || "", }); - // Check if location is needed and handle accordingly + // Auto-apply user's saved address if no location is set useEffect(() => { // Only run this check once per mount if (locationCheckDone.current) return; const hasLocation = searchParams.has("lat") || searchParams.has("city") || searchParams.has("zipCode"); - const hasSearchTerm = searchParams.has("search"); if (!hasLocation) { // Check user's saved address for lat/lng @@ -47,12 +48,7 @@ const ItemList: React.FC = () => { params.set("radius", "25"); locationCheckDone.current = true; navigate(`/items?${params.toString()}`, { replace: true }); - } else if (!hasSearchTerm) { - // No saved address and no search term - show location prompt - locationCheckDone.current = true; - setShowLocationPrompt(true); } else { - // Has search term but no location - just show results without location filter locationCheckDone.current = true; } } else { @@ -74,22 +70,39 @@ const ItemList: React.FC = () => { lng: searchParams.get("lng") || "", radius: searchParams.get("radius") || "", }); + setLocationName(searchParams.get("locationName") || ""); }, [searchParams]); - const handleLocationSelect = (location: { lat: number; lng: number }) => { + const handleApplyFilters = (newFilters: { + lat: string; + lng: string; + radius: string; + locationName: string; + }) => { const params = new URLSearchParams(searchParams); - params.set("lat", location.lat.toString()); - params.set("lng", location.lng.toString()); - params.set("radius", "25"); - // Remove city/zipCode since we're using coordinates - params.delete("city"); - params.delete("zipCode"); + if (newFilters.lat && newFilters.lng) { + params.set("lat", newFilters.lat); + params.set("lng", newFilters.lng); + params.set("radius", newFilters.radius || "25"); + params.set("locationName", newFilters.locationName); + // Remove city/zipCode since we're using coordinates + params.delete("city"); + params.delete("zipCode"); + } else { + // Clear location filters + params.delete("lat"); + params.delete("lng"); + params.delete("radius"); + params.delete("locationName"); + } + setLocationName(newFilters.locationName); navigate(`/items?${params.toString()}`, { replace: true }); - setShowLocationPrompt(false); }; + const hasActiveLocationFilter = filters.lat && filters.lng; + const fetchItems = async () => { try { setLoading(true); @@ -164,26 +177,58 @@ const ItemList: React.FC = () => { {items.length} items found - {items.length > 0 && ( -
+
+ {/* Filter Button */} +
- + setShowFilterPanel(false)} + currentFilters={{ + lat: filters.lat, + lng: filters.lng, + radius: filters.radius, + locationName: locationName, + }} + onApplyFilters={handleApplyFilters} + />
- )} + + {/* View Toggle */} + {items.length > 0 && ( +
+ + +
+ )} +
{items.length === 0 ? ( @@ -215,12 +260,6 @@ const ItemList: React.FC = () => { /> )} - - setShowLocationPrompt(false)} - onLocationSelect={handleLocationSelect} - /> ); };