import React, { useState, useRef, useEffect } from "react";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import pinIconImage from "../images/pin.png";
import Navbar from "./Navbar";
import { useLocation, useNavigate } from 'react-router-dom';
// Utility function to introduce a delay
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const Pin = () => {
// Address search state
const [address, setAddress] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [selectedLocation, setSelectedLocation] = useState(null);
const [fullAddress, setFullAddress] = useState("");
// Pin state
const [pins, setPins] = useState([]);
const [username, setUsername] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [pinMode, setPinMode] = useState(false);
const [showPinForm, setShowPinForm] = useState(false);
const [currentPinLocation, setCurrentPinLocation] = useState(null);
const [pinDetails, setPinDetails] = useState({
pin_name: '',
description: '',
category: '',
public: false,
x: 0,
y: 0
});
const mapRef = useRef(null);
const mapInstance = useRef(null);
const markersRef = useRef([]);
const location = useLocation();
const navigate = useNavigate();
// Custom pin icon
const pinIcon = new L.Icon({
iconUrl: pinIconImage,
iconSize: [24, 40],
iconAnchor: [15, 40],
popupAnchor: [0, -40],
});
// Initialize map and fetch user data
useEffect(() => {
// Check login status
const storedUsername = localStorage.getItem('username');
const storedLoginStatus = localStorage.getItem('login');
if (storedUsername && storedLoginStatus === 'true') {
setUsername(storedUsername);
setIsLoggedIn(true);
fetchPins(storedUsername);
} else {
fetchPins(null);
}
// Initialize map
if (!mapInstance.current) {
const initialCenter = location.state?.pinLocation || [43.7, -79.42];
const initialZoom = location.state?.pinLocation ? 14 : 10;
const map = L.map(mapRef.current, {
center: initialCenter,
zoom: initialZoom,
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
}).addTo(map);
mapInstance.current = map;
}
// Cleanup markers on unmount
return () => {
markersRef.current.forEach(marker => {
if (marker && mapInstance.current) {
mapInstance.current.removeLayer(marker);
}
});
};
}, []);
// Handle incoming pin location from state
useEffect(() => {
if (location.state?.pinLocation && mapInstance.current) {
const { lat, lng } = location.state.pinLocation;
mapInstance.current.flyTo([lat, lng], 14);
// Clear the state after handling it to prevent re-centering
navigate('/visit', { replace: true, state: {} });
}
}, [location.state, navigate]);
// Handle map click for pin placement
useEffect(() => {
if (!mapInstance.current) return;
const handleMapClick = (e) => {
if (pinMode) {
const { lat, lng } = e.latlng;
setCurrentPinLocation({ lat, lng });
setPinDetails(prev => ({
...prev,
x: lng,
y: lat
}));
setShowPinForm(true);
}
};
// Add click event listener
mapInstance.current.on('click', handleMapClick);
// Cleanup
return () => {
if (mapInstance.current) {
mapInstance.current.off('click', handleMapClick);
}
};
}, [pinMode]);
// Update pins on the map when pins state changes
useEffect(() => {
if (!mapInstance.current) return;
// Clear existing markers
markersRef.current.forEach(marker => {
if (marker) {
mapInstance.current.removeLayer(marker);
}
});
markersRef.current = [];
// Add new markers
pins.forEach((pin) => {
const marker = L.marker([pin.y, pin.x], { icon: pinIcon })
.addTo(mapInstance.current)
.bindPopup(`
<strong>${pin.pin_name || "Unnamed Pin"}</strong>
<br />
${pin.description || "No description available"}
<br />
<em>By: ${pin.username}</em>
<br />
<small>Category: ${pin.category || "Unknown"}</small>
${pin.public ?
'<div><small>Status: Public</small></div>' :
'<div><small>Status: Private (only visible to you)</small></div>'}
`);
markersRef.current.push(marker);
});
}, [pins]);
// Fetch pins from server
const fetchPins = (username) => {
fetch(`https://jp-backend-kc80.onrender.com/api/pins`)
.then(response => response.json())
.then(data => {
// Filter pins: show public ones OR private ones belonging to the logged-in user
const filteredPins = data.filter(pin =>
pin.public || (username && pin.username === username)
);
setPins(filteredPins);
})
.catch(error => console.error("Error fetching pins:", error));
};
// Handle form input changes
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setPinDetails(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
// Function to save pin with details
const savePinWithDetails = () => {
if (!currentPinLocation || !username) return;
const newPin = {
...pinDetails,
username: username,
x: currentPinLocation.lng,
y: currentPinLocation.lat
};
// Send to backend
fetch(`https://jp-backend-kc80.onrender.com/api/pins?username=${username}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pin_name: pinDetails.pin_name,
username: username,
x: currentPinLocation.lng,
y: currentPinLocation.lat,
description: pinDetails.description,
category: pinDetails.category,
public: pinDetails.public
}),
})
.then((response) => response.json())
.then((data) => {
console.log("Pin saved:", data);
setPins((prevPins) => [...prevPins, newPin]);
setShowPinForm(false);
setPinDetails({
pin_name: '',
description: '',
category: '',
public: false,
x: 0,
y: 0
});
fetchPins(username);
})
.catch((error) => {
console.error("Error saving pin:", error);
alert("Failed to save pin. Please try again.");
});
};
// Handle input change for address search
const handleInputChangeAddress = async (e) => {
const value = e.target.value;
setAddress(value);
if (value.length >= 1) {
try {
await delay(2000);
const response = await fetch(
`https://photon.komoot.io/api/?q=${value}&limit=5`
);
const data = await response.json();
setSuggestions(data.features);
} catch (error) {
console.error("Error fetching data from Photon:", error);
}
} else {
setSuggestions([]);
}
};
// Handle the user selecting a suggestion
const handleSuggestionClick = (suggestion) => {
const [lon, lat] = suggestion.geometry.coordinates;
setSelectedLocation({ lat, lon });
setSuggestions([]);
const addressString = [
suggestion.properties.name,
suggestion.properties.city,
suggestion.properties.postcode,
suggestion.properties.country,
]
.filter((part) => part)
.join(", ");
setFullAddress(addressString);
setAddress(addressString);
};
// Handle the user clicking the "Visit" button
const handleVisitClick = () => {
if (selectedLocation && mapInstance.current) {
const map = mapInstance.current;
map.flyTo([selectedLocation.lat, selectedLocation.lon], 14);
}
};
return (
<div>
<Navbar />
<div className="container-fluid">
<div className="row row-cols-2 vh-100">
<div className="col-7 px-0">
<div
id="mapRef"
ref={mapRef}
style={{ height: "100%", width: "100%" }}
></div>
{/* Floating pin button */}
<button
className={`pin-button ${pinMode ? 'active' : ''}`}
onClick={() => setPinMode(!pinMode)}
disabled={!isLoggedIn}
title={!isLoggedIn ? "Please log in to add pins" : ""}
style={{
position: 'absolute',
bottom: '20px',
left: '20px',
zIndex: 1000,
backgroundColor: pinMode ? '#154475' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '50%',
width: '50px',
height: '50px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}
>
<img src={pinIconImage} alt="Pin" width="20" height="35" />
</button>
</div>
<div className="col-5 pt-3">
<form className="row g-3">
<div className="col-12">
<label htmlFor="inputAddress" className="form-label">
Address
</label>
<input
type="text"
className="form-control"
id="inputAddress"
placeholder="Toronto, Ontario, Canada"
value={address}
onChange={handleInputChangeAddress}
/>
{suggestions.length > 0 && address.trim().length >= 1 && (
<ul
className="list-group mt-2"
style={{ maxHeight: "200px", overflowY: "auto" }}
>
{suggestions.map((suggestion, index) => {
const formattedAddress = [
suggestion.properties.name,
suggestion.properties.city,
suggestion.properties.postcode,
suggestion.properties.country,
]
.filter((part) => part)
.join(", ");
return (
<li
key={index}
className="list-group-item list-group-item-action"
onClick={() => handleSuggestionClick(suggestion)}
>
{formattedAddress}
</li>
);
})}
</ul>
)}
</div>
<div className="col-12">
<button
type="button"
className="btn btn-primary"
onClick={handleVisitClick}
>
Visit
</button>
</div>
</form>
</div>
</div>
</div>
{/* Pin Details Form Modal */}
{showPinForm && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '400px',
maxWidth: '90%'
}}>
<h3>Add Pin Details</h3>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Pin Name:
</label>
<input
type="text"
name="pin_name"
value={pinDetails.pin_name}
onChange={handleInputChange}
required
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Description:
</label>
<textarea
name="description"
value={pinDetails.description}
onChange={handleInputChange}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
height: '80px'
}}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Category:
</label>
<select
name="category"
value={pinDetails.category}
onChange={handleInputChange}
required
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="">Select a category</option>
<option value="Landmark">Landmark</option>
<option value="Restaurant">Restaurant</option>
<option value="Nature">Nature</option>
<option value="Event">Event</option>
<option value="Other">Other</option>
</select>
</div>
<div style={{ marginBottom: '15px', display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
name="public"
checked={pinDetails.public}
onChange={handleInputChange}
style={{ width: 'auto', marginRight: '10px' }}
/>
<label>Public Pin</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Coordinates:
</label>
<p>X (Longitude): {pinDetails.x.toFixed(5)}</p>
<p>Y (Latitude): {pinDetails.y.toFixed(5)}</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
<button
onClick={() => setShowPinForm(false)}
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
onClick={savePinWithDetails}
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: '#007bff',
color: 'white'
}}
>
Save Pin
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Pin;