Updated search bar to remove location. Will get or ask for user's location. Removed Start Earning button. Works on desktop and mobile
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const express = require("express");
|
||||
const { Op } = require("sequelize");
|
||||
const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations
|
||||
const { Op, Sequelize } = require("sequelize");
|
||||
const { Item, User, Rental, sequelize } = require("../models"); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken, requireVerifiedEmail, requireAdmin, optionalAuth } = require("../middleware/auth");
|
||||
const logger = require("../utils/logger");
|
||||
const { validateS3Keys } = require("../utils/s3KeyValidator");
|
||||
@@ -61,6 +61,9 @@ router.get("/", async (req, res, next) => {
|
||||
city,
|
||||
zipCode,
|
||||
search,
|
||||
lat,
|
||||
lng,
|
||||
radius = 25,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
} = req.query;
|
||||
@@ -74,8 +77,50 @@ router.get("/", async (req, res, next) => {
|
||||
if (minPrice) where.pricePerDay[Op.gte] = minPrice;
|
||||
if (maxPrice) where.pricePerDay[Op.lte] = maxPrice;
|
||||
}
|
||||
|
||||
// Location filtering: Radius search OR city/ZIP fallback
|
||||
if (lat && lng) {
|
||||
// Parse and validate coordinates
|
||||
const latNum = parseFloat(lat);
|
||||
const lngNum = parseFloat(lng);
|
||||
const radiusNum = parseFloat(radius);
|
||||
|
||||
if (!isNaN(latNum) && !isNaN(lngNum) && !isNaN(radiusNum)) {
|
||||
// Bounding box pre-filter (fast, uses indexes)
|
||||
// ~69 miles per degree latitude, longitude varies by latitude
|
||||
const latDelta = radiusNum / 69;
|
||||
const lngDelta = radiusNum / (69 * Math.cos(latNum * Math.PI / 180));
|
||||
|
||||
where.latitude = {
|
||||
[Op.and]: [
|
||||
{ [Op.gte]: latNum - latDelta },
|
||||
{ [Op.lte]: latNum + latDelta },
|
||||
{ [Op.ne]: null }
|
||||
]
|
||||
};
|
||||
where.longitude = {
|
||||
[Op.and]: [
|
||||
{ [Op.gte]: lngNum - lngDelta },
|
||||
{ [Op.lte]: lngNum + lngDelta },
|
||||
{ [Op.ne]: null }
|
||||
]
|
||||
};
|
||||
|
||||
// Haversine formula for exact distance (applied after bounding box)
|
||||
// 3959 = Earth's radius in miles
|
||||
where[Op.and] = sequelize.literal(`
|
||||
(3959 * acos(
|
||||
cos(radians(${latNum})) * cos(radians("Item"."latitude")) *
|
||||
cos(radians("Item"."longitude") - radians(${lngNum})) +
|
||||
sin(radians(${latNum})) * sin(radians("Item"."latitude"))
|
||||
)) <= ${radiusNum}
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
// Fallback to city/ZIP string matching
|
||||
if (city) where.city = { [Op.iLike]: `%${city}%` };
|
||||
if (zipCode) where.zipCode = { [Op.iLike]: `%${zipCode}%` };
|
||||
}
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.iLike]: `%${search}%` } },
|
||||
@@ -113,7 +158,7 @@ router.get("/", async (req, res, next) => {
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Items search completed", {
|
||||
filters: { minPrice, maxPrice, city, zipCode, search },
|
||||
filters: { minPrice, maxPrice, city, zipCode, search, lat, lng, radius },
|
||||
resultsCount: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
|
||||
@@ -8,11 +8,6 @@ main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-brand i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
@@ -22,3 +17,41 @@ main {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* Navbar search - centered */
|
||||
.navbar-search {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Navbar layout - center search bar */
|
||||
.navbar .container-fluid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar .navbar-collapse {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-search {
|
||||
max-width: none;
|
||||
flex: 1;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
.navbar-search .form-control {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.navbar .container-fluid {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
170
frontend/src/components/LocationPromptModal.tsx
Normal file
170
frontend/src/components/LocationPromptModal.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface LocationPromptModalProps {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
onLocationSelect: (
|
||||
location: { lat: number; lng: number } | { city?: string; zipCode?: string }
|
||||
) => 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 = () => {
|
||||
const trimmed = manualLocation.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// Check if it looks like a ZIP code
|
||||
if (/^\d{5}(-\d{4})?$/.test(trimmed)) {
|
||||
onLocationSelect({ zipCode: trimmed });
|
||||
} else {
|
||||
onLocationSelect({ city: trimmed });
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -8,10 +8,7 @@ const Navbar: React.FC = () => {
|
||||
const { user, logout, openAuthModal } = useAuth();
|
||||
const { onNewMessage, onMessageRead } = useSocket();
|
||||
const navigate = useNavigate();
|
||||
const [searchFilters, setSearchFilters] = useState({
|
||||
search: "",
|
||||
location: "",
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
|
||||
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
|
||||
|
||||
@@ -40,7 +37,10 @@ const Navbar: React.FC = () => {
|
||||
window.addEventListener("rentalStatusChanged", handleRentalStatusChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("rentalStatusChanged", handleRentalStatusChange);
|
||||
window.removeEventListener(
|
||||
"rentalStatusChanged",
|
||||
handleRentalStatusChange
|
||||
);
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
@@ -93,31 +93,15 @@ const Navbar: React.FC = () => {
|
||||
e?.preventDefault();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (searchFilters.search.trim()) {
|
||||
params.append("search", searchFilters.search.trim());
|
||||
}
|
||||
if (searchFilters.location.trim()) {
|
||||
// Check if location looks like a zip code (5 digits) or city name
|
||||
const location = searchFilters.location.trim();
|
||||
if (/^\d{5}(-\d{4})?$/.test(location)) {
|
||||
params.append("zipCode", location);
|
||||
} else {
|
||||
params.append("city", location);
|
||||
}
|
||||
if (searchTerm.trim()) {
|
||||
params.append("search", searchTerm.trim());
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
navigate(`/items${queryString ? `?${queryString}` : ""}`);
|
||||
// Location is handled in ItemList
|
||||
navigate(`/items${params.toString() ? `?${params.toString()}` : ""}`);
|
||||
|
||||
// Clear search after navigating
|
||||
setSearchFilters({ search: "", location: "" });
|
||||
};
|
||||
|
||||
const handleSearchInputChange = (
|
||||
field: "search" | "location",
|
||||
value: string
|
||||
) => {
|
||||
setSearchFilters((prev) => ({ ...prev, [field]: value }));
|
||||
setSearchTerm("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -125,57 +109,18 @@ const Navbar: React.FC = () => {
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||
<div className="container-fluid" style={{ maxWidth: "1800px" }}>
|
||||
<Link className="navbar-brand fw-bold" to="/">
|
||||
<i className="bi bi-box-seam me-2"></i>
|
||||
Village Share
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<div className="d-flex align-items-center w-100">
|
||||
<div className="position-absolute start-50 translate-middle-x">
|
||||
<div>
|
||||
<div className="input-group" style={{ width: "520px" }}>
|
||||
|
||||
{/* Search bar - always visible, centered */}
|
||||
<div className="navbar-search">
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search items..."
|
||||
value={searchFilters.search}
|
||||
onChange={(e) =>
|
||||
handleSearchInputChange("search", e.target.value)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="input-group-text text-muted"
|
||||
style={{
|
||||
borderLeft: "0",
|
||||
borderRight: "1px solid #dee2e6",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
>
|
||||
in
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="City or ZIP"
|
||||
value={searchFilters.location}
|
||||
onChange={(e) =>
|
||||
handleSearchInputChange("location", e.target.value)
|
||||
}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch(e);
|
||||
@@ -191,14 +136,56 @@ const Navbar: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-auto d-flex align-items-center">
|
||||
<Link
|
||||
className="btn btn-outline-primary btn-sm me-3 text-nowrap"
|
||||
to="/create-item"
|
||||
|
||||
{/* Mobile avatar toggle */}
|
||||
<button
|
||||
className="navbar-toggler border-0 p-0"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
Start Earning
|
||||
</Link>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{(pendingRequestsCount > 0 || unreadMessagesCount > 0) && (
|
||||
<span
|
||||
className="mobile-notification-badge"
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "-5px",
|
||||
top: "-5px",
|
||||
backgroundColor: "#dc3545",
|
||||
color: "white",
|
||||
borderRadius: "50%",
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.7em",
|
||||
fontWeight: "bold",
|
||||
border: "2px solid white",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{pendingRequestsCount + unreadMessagesCount}
|
||||
</span>
|
||||
)}
|
||||
<i
|
||||
className="bi bi-person-circle"
|
||||
style={{ fontSize: "1.5rem", color: "#333" }}
|
||||
></i>
|
||||
</span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<div className="d-flex align-items-center ms-auto">
|
||||
<ul className="navbar-nav flex-row">
|
||||
{user ? (
|
||||
<>
|
||||
@@ -211,8 +198,15 @@ const Navbar: React.FC = () => {
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center", position: "relative" }}>
|
||||
{(pendingRequestsCount > 0 || unreadMessagesCount > 0) && (
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{(pendingRequestsCount > 0 ||
|
||||
unreadMessagesCount > 0) && (
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -317,7 +311,6 @@ const Navbar: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,61 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
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 { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
const ItemList: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'list' | 'map'>('list');
|
||||
const [showLocationPrompt, setShowLocationPrompt] = useState(false);
|
||||
const locationCheckDone = useRef(false);
|
||||
const [filters, setFilters] = useState({
|
||||
search: searchParams.get("search") || "",
|
||||
city: searchParams.get("city") || "",
|
||||
zipCode: searchParams.get("zipCode") || "",
|
||||
lat: searchParams.get("lat") || "",
|
||||
lng: searchParams.get("lng") || "",
|
||||
radius: searchParams.get("radius") || "",
|
||||
});
|
||||
|
||||
// Check if location is needed and handle accordingly
|
||||
useEffect(() => {
|
||||
// Only run this check once per mount
|
||||
if (locationCheckDone.current) return;
|
||||
|
||||
const hasLocation = searchParams.has("lat") || searchParams.has("city") || searchParams.has("zipCode");
|
||||
|
||||
if (!hasLocation) {
|
||||
// Check user's saved address for lat/lng
|
||||
const userLat = user?.addresses?.[0]?.latitude;
|
||||
const userLng = user?.addresses?.[0]?.longitude;
|
||||
|
||||
if (userLat && userLng) {
|
||||
// Use saved address coordinates
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("lat", userLat.toString());
|
||||
params.set("lng", userLng.toString());
|
||||
params.set("radius", "25");
|
||||
locationCheckDone.current = true;
|
||||
navigate(`/items?${params.toString()}`, { replace: true });
|
||||
} else {
|
||||
// No saved address with coordinates - show location prompt
|
||||
locationCheckDone.current = true;
|
||||
setShowLocationPrompt(true);
|
||||
}
|
||||
} else {
|
||||
locationCheckDone.current = true;
|
||||
}
|
||||
}, [user, searchParams, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, [filters]);
|
||||
@@ -28,9 +66,37 @@ const ItemList: React.FC = () => {
|
||||
search: searchParams.get("search") || "",
|
||||
city: searchParams.get("city") || "",
|
||||
zipCode: searchParams.get("zipCode") || "",
|
||||
lat: searchParams.get("lat") || "",
|
||||
lng: searchParams.get("lng") || "",
|
||||
radius: searchParams.get("radius") || "",
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
const handleLocationSelect = (
|
||||
location: { lat: number; lng: number } | { city?: string; zipCode?: string }
|
||||
) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
if ("lat" in location) {
|
||||
params.set("lat", location.lat.toString());
|
||||
params.set("lng", location.lng.toString());
|
||||
params.set("radius", "25");
|
||||
// Remove city/zipCode if using coordinates
|
||||
params.delete("city");
|
||||
params.delete("zipCode");
|
||||
} else {
|
||||
if (location.city) params.set("city", location.city);
|
||||
if (location.zipCode) params.set("zipCode", location.zipCode);
|
||||
// Remove lat/lng if using city/zip
|
||||
params.delete("lat");
|
||||
params.delete("lng");
|
||||
params.delete("radius");
|
||||
}
|
||||
|
||||
navigate(`/items?${params.toString()}`, { replace: true });
|
||||
setShowLocationPrompt(false);
|
||||
};
|
||||
|
||||
const fetchItems = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -66,6 +132,10 @@ const ItemList: React.FC = () => {
|
||||
};
|
||||
|
||||
const getSearchLocationString = () => {
|
||||
if (filters.lat && filters.lng) {
|
||||
// When using coordinates, return them as a string for the map
|
||||
return `${filters.lat},${filters.lng}`;
|
||||
}
|
||||
if (filters.city) return filters.city;
|
||||
if (filters.zipCode) return filters.zipCode;
|
||||
return '';
|
||||
@@ -152,6 +222,12 @@ const ItemList: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LocationPromptModal
|
||||
show={showLocationPrompt}
|
||||
onClose={() => setShowLocationPrompt(false)}
|
||||
onLocationSelect={handleLocationSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user