location filter
This commit is contained in:
222
frontend/src/components/FilterPanel.tsx
Normal file
222
frontend/src/components/FilterPanel.tsx
Normal file
@@ -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<FilterPanelProps> = ({
|
||||||
|
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<HTMLDivElement>(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 (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
.filter-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
width: 320px;
|
||||||
|
min-width: 320px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1050;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
animation: filterPanelSlideDown 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes filterPanelSlideDown {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.filter-panel {
|
||||||
|
position: fixed;
|
||||||
|
width: calc(100vw - 2rem);
|
||||||
|
min-width: unset;
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
top: auto;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<div ref={panelRef} className="filter-panel">
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label fw-medium mb-2">Location</label>
|
||||||
|
<AddressAutocomplete
|
||||||
|
value={locationInput}
|
||||||
|
onChange={setLocationInput}
|
||||||
|
onPlaceSelect={handlePlaceSelect}
|
||||||
|
placeholder="City, ZIP Code, or Address"
|
||||||
|
types={["(regions)"]}
|
||||||
|
countryRestriction="us"
|
||||||
|
id="filter-location"
|
||||||
|
/>
|
||||||
|
{selectedLocation && (
|
||||||
|
<div className="mt-2 small text-muted d-flex align-items-center">
|
||||||
|
<i className="bi bi-check-circle text-success me-1"></i>
|
||||||
|
{selectedLocation.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label fw-medium mb-2">Search Radius</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={radius}
|
||||||
|
onChange={(e) => setRadius(e.target.value)}
|
||||||
|
>
|
||||||
|
{RADIUS_OPTIONS.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-top p-3 d-flex justify-content-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link text-muted text-decoration-none px-0"
|
||||||
|
onClick={handleClear}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleApply}
|
||||||
|
disabled={!selectedLocation}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterPanel;
|
||||||
@@ -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<LocationPromptModalProps> = ({
|
|
||||||
show,
|
|
||||||
onClose,
|
|
||||||
onLocationSelect,
|
|
||||||
}) => {
|
|
||||||
const [manualLocation, setManualLocation] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
if (!show) return null;
|
|
||||||
|
|
||||||
const handleUseMyLocation = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const position = await new Promise<GeolocationPosition>(
|
|
||||||
(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<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleManualSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="modal d-block"
|
|
||||||
tabIndex={-1}
|
|
||||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
||||||
>
|
|
||||||
<div className="modal-dialog modal-dialog-centered">
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
<h5 className="modal-title">
|
|
||||||
<i className="bi bi-geo-alt me-2"></i>
|
|
||||||
Where are you looking?
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={loading}
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<p className="text-muted mb-4">
|
|
||||||
Help us show you items nearby
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary w-100 mb-3 d-flex align-items-center justify-content-center"
|
|
||||||
onClick={handleUseMyLocation}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="spinner-border spinner-border-sm me-2"
|
|
||||||
role="status"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
Getting location...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<i className="bi bi-crosshair me-2"></i>
|
|
||||||
Use my current location
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="alert alert-warning py-2 small">
|
|
||||||
<i className="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="d-flex align-items-center my-3">
|
|
||||||
<hr className="flex-grow-1" />
|
|
||||||
<span className="px-3 text-muted">or</span>
|
|
||||||
<hr className="flex-grow-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="Enter city or ZIP code"
|
|
||||||
value={manualLocation}
|
|
||||||
onChange={(e) => setManualLocation(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleManualSubmit}
|
|
||||||
disabled={loading || !manualLocation.trim()}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LocationPromptModal;
|
|
||||||
@@ -4,7 +4,7 @@ import { Item } from "../types";
|
|||||||
import { itemAPI } from "../services/api";
|
import { itemAPI } from "../services/api";
|
||||||
import ItemCard from "../components/ItemCard";
|
import ItemCard from "../components/ItemCard";
|
||||||
import SearchResultsMap from "../components/SearchResultsMap";
|
import SearchResultsMap from "../components/SearchResultsMap";
|
||||||
import LocationPromptModal from "../components/LocationPromptModal";
|
import FilterPanel from "../components/FilterPanel";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
const ItemList: React.FC = () => {
|
const ItemList: React.FC = () => {
|
||||||
@@ -15,8 +15,10 @@ const ItemList: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [viewMode, setViewMode] = useState<'list' | 'map'>('list');
|
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 locationCheckDone = useRef(false);
|
||||||
|
const filterButtonRef = useRef<HTMLDivElement>(null);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
search: searchParams.get("search") || "",
|
search: searchParams.get("search") || "",
|
||||||
city: searchParams.get("city") || "",
|
city: searchParams.get("city") || "",
|
||||||
@@ -26,13 +28,12 @@ const ItemList: React.FC = () => {
|
|||||||
radius: searchParams.get("radius") || "",
|
radius: searchParams.get("radius") || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if location is needed and handle accordingly
|
// Auto-apply user's saved address if no location is set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run this check once per mount
|
// Only run this check once per mount
|
||||||
if (locationCheckDone.current) return;
|
if (locationCheckDone.current) return;
|
||||||
|
|
||||||
const hasLocation = searchParams.has("lat") || searchParams.has("city") || searchParams.has("zipCode");
|
const hasLocation = searchParams.has("lat") || searchParams.has("city") || searchParams.has("zipCode");
|
||||||
const hasSearchTerm = searchParams.has("search");
|
|
||||||
|
|
||||||
if (!hasLocation) {
|
if (!hasLocation) {
|
||||||
// Check user's saved address for lat/lng
|
// Check user's saved address for lat/lng
|
||||||
@@ -47,12 +48,7 @@ const ItemList: React.FC = () => {
|
|||||||
params.set("radius", "25");
|
params.set("radius", "25");
|
||||||
locationCheckDone.current = true;
|
locationCheckDone.current = true;
|
||||||
navigate(`/items?${params.toString()}`, { replace: 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 {
|
} else {
|
||||||
// Has search term but no location - just show results without location filter
|
|
||||||
locationCheckDone.current = true;
|
locationCheckDone.current = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -74,22 +70,39 @@ const ItemList: React.FC = () => {
|
|||||||
lng: searchParams.get("lng") || "",
|
lng: searchParams.get("lng") || "",
|
||||||
radius: searchParams.get("radius") || "",
|
radius: searchParams.get("radius") || "",
|
||||||
});
|
});
|
||||||
|
setLocationName(searchParams.get("locationName") || "");
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const handleLocationSelect = (location: { lat: number; lng: number }) => {
|
const handleApplyFilters = (newFilters: {
|
||||||
|
lat: string;
|
||||||
|
lng: string;
|
||||||
|
radius: string;
|
||||||
|
locationName: string;
|
||||||
|
}) => {
|
||||||
const params = new URLSearchParams(searchParams);
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
params.set("lat", location.lat.toString());
|
if (newFilters.lat && newFilters.lng) {
|
||||||
params.set("lng", location.lng.toString());
|
params.set("lat", newFilters.lat);
|
||||||
params.set("radius", "25");
|
params.set("lng", newFilters.lng);
|
||||||
// Remove city/zipCode since we're using coordinates
|
params.set("radius", newFilters.radius || "25");
|
||||||
params.delete("city");
|
params.set("locationName", newFilters.locationName);
|
||||||
params.delete("zipCode");
|
// 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 });
|
navigate(`/items?${params.toString()}`, { replace: true });
|
||||||
setShowLocationPrompt(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasActiveLocationFilter = filters.lat && filters.lng;
|
||||||
|
|
||||||
const fetchItems = async () => {
|
const fetchItems = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -164,26 +177,58 @@ const ItemList: React.FC = () => {
|
|||||||
<span className="text-muted">{items.length} items found</span>
|
<span className="text-muted">{items.length} items found</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.length > 0 && (
|
<div className="d-flex align-items-center gap-2">
|
||||||
<div className="btn-group" role="group" aria-label="View toggle">
|
{/* Filter Button */}
|
||||||
|
<div className="position-relative" ref={filterButtonRef} style={{ overflow: 'visible' }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-outline-secondary ${viewMode === 'list' ? 'active' : ''}`}
|
className={`btn btn-outline-secondary ${hasActiveLocationFilter ? 'active' : ''}`}
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => setShowFilterPanel(!showFilterPanel)}
|
||||||
|
data-filter-button
|
||||||
>
|
>
|
||||||
<i className="bi bi-list-ul me-1 me-md-2"></i>
|
<i className="bi bi-filter me-1 me-md-2 align-middle"></i>
|
||||||
<span className="d-none d-md-inline">List</span>
|
<span className="d-none d-md-inline">Filters</span>
|
||||||
</button>
|
{hasActiveLocationFilter && (
|
||||||
<button
|
<span className="position-absolute top-0 start-100 translate-middle p-1 bg-primary border border-light rounded-circle">
|
||||||
type="button"
|
<span className="visually-hidden">Active filters</span>
|
||||||
className={`btn btn-outline-secondary ${viewMode === 'map' ? 'active' : ''}`}
|
</span>
|
||||||
onClick={() => setViewMode('map')}
|
)}
|
||||||
>
|
|
||||||
<i className="bi bi-geo-alt-fill me-1 me-md-2"></i>
|
|
||||||
<span className="d-none d-md-inline">Map</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
<FilterPanel
|
||||||
|
show={showFilterPanel}
|
||||||
|
onClose={() => setShowFilterPanel(false)}
|
||||||
|
currentFilters={{
|
||||||
|
lat: filters.lat,
|
||||||
|
lng: filters.lng,
|
||||||
|
radius: filters.radius,
|
||||||
|
locationName: locationName,
|
||||||
|
}}
|
||||||
|
onApplyFilters={handleApplyFilters}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* View Toggle */}
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="btn-group" role="group" aria-label="View toggle">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-outline-secondary ${viewMode === 'list' ? 'active' : ''}`}
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
>
|
||||||
|
<i className="bi bi-list-ul me-1 me-md-2"></i>
|
||||||
|
<span className="d-none d-md-inline">List</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-outline-secondary ${viewMode === 'map' ? 'active' : ''}`}
|
||||||
|
onClick={() => setViewMode('map')}
|
||||||
|
>
|
||||||
|
<i className="bi bi-geo-alt-fill me-1 me-md-2"></i>
|
||||||
|
<span className="d-none d-md-inline">Map</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
@@ -215,12 +260,6 @@ const ItemList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LocationPromptModal
|
|
||||||
show={showLocationPrompt}
|
|
||||||
onClose={() => setShowLocationPrompt(false)}
|
|
||||||
onLocationSelect={handleLocationSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user