JourneyPoint / journeypoint / frontend / src / Components / Visit.js
Visit.js
Raw
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: "&copy; <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;