diff --git a/.idea/misc.xml b/.idea/misc.xml index cd5457df..e8311ab3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,9 +3,4 @@ - - - \ No newline at end of file diff --git a/backend/src/main/resources/dev-data/waste.json b/backend/src/main/resources/dev-data/waste.json index 2f3f1164..eeee5255 100644 --- a/backend/src/main/resources/dev-data/waste.json +++ b/backend/src/main/resources/dev-data/waste.json @@ -134,7 +134,6 @@ {"name": "Skap", "type": "Brennbart avfall", "info": "???"}, {"name": "Seng", "type": "Brennbart avfall", "info": "???"}, {"name": "Madrass", "type": "Brennbart avfall", "info": "???"}, - {"name": "Teppe", "type": "Brennbart avfall", "info": "???"}, {"name": "Speil", "type": "Restavfall", "info": "???"}, {"name": "Sykkel", "type": "Brennbart avfall", "info": "???"}, {"name": "Barnevogn", "type": "Brennbart avfall", "info": "???"}, diff --git a/frontend/sortify/app/mainContent.module.css b/frontend/sortify/app/mainContent.module.css index 6edf2f0a..87eceeb1 100644 --- a/frontend/sortify/app/mainContent.module.css +++ b/frontend/sortify/app/mainContent.module.css @@ -7,6 +7,7 @@ flex-direction: column; align-items: center; margin: 0 auto; + flex-grow: 1; } .title { diff --git a/frontend/sortify/app/page.tsx b/frontend/sortify/app/page.tsx index 4bf0543e..a848de95 100644 --- a/frontend/sortify/app/page.tsx +++ b/frontend/sortify/app/page.tsx @@ -6,11 +6,10 @@ const Map = dynamic(() => import('../components/map'), {ssr: false}); import { useSearch } from "@/app/context/searchContext"; export default function Page(){ - const { search } = useSearch(); return(
-

Recycling map

- +

Gjenvinningskart

+
); } \ No newline at end of file diff --git a/frontend/sortify/components/footer/footer.tsx b/frontend/sortify/components/footer/footer.tsx index 7703d107..2761c2ef 100644 --- a/frontend/sortify/components/footer/footer.tsx +++ b/frontend/sortify/components/footer/footer.tsx @@ -14,7 +14,7 @@ export default function Footer() { />

- © 2025 Sortify. All rights reserved. + © 2025 Sortify.

diff --git a/frontend/sortify/components/map.css b/frontend/sortify/components/map.css new file mode 100644 index 00000000..691a9c83 --- /dev/null +++ b/frontend/sortify/components/map.css @@ -0,0 +1,131 @@ +/* map.css */ + +.container { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 800px; + position: relative; +} + +/* Wrap map and filters horizontally */ +.map-and-filters { + display: flex; + flex-direction: row; + width: 100%; +} + +.filter-panel { + background-color: #DBBC99; + padding: 20px; + border-radius: 15px; + border: 2px solid #3E4739; /* Dark green border for consistency */ + max-width: 180px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 10px; + font-size: 14px; + color: #3E4739; + margin-left: 20px; /* Space between map and panel */ + height: 100%; + margin-top: 12px; +} + +/* Optional: Style for each filter checkbox line */ +.filter-panel label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +/* Adjust map width to allow space for filter panel */ +.map { + flex-grow: 1; + height: 500px; +} + +/* Route button with .signupButton styles */ +.route-button { + display: inline-block; + background-color: #D9996B; + color: #3E4739; + padding: 12px 24px; + border-radius: 30px; + text-decoration: none; + font-weight: 600; + text-transform: uppercase; + font-size: 14px; + transition: all 0.3s ease; + cursor: pointer; + box-sizing: border-box; + border: 2px solid #3E4739; +} + +/* Hover effect for the route button */ +.route-button:hover { + background-color: #B6CBBC; +} + +/* Disabled route button style */ +.route-button:disabled { + background-color: #ddd; + cursor: not-allowed; + border: 2px solid #ccc; +} + +/* Container for the info box and button */ +.info-box-container { + background-color: #DBBC99; /* Warm beige color for the container */ + padding: 20px; + border-radius: 8px; + margin-top: 20px; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 220px; /* Limit the width of the container */ + width: 100%; /* Makes sure it is responsive */ + margin: 0 auto; /* Centers the container horizontally */ +} + +/* Styling for the info box */ +.info-box { + border: 2px solid #3E4739; /* Dark green border for contrast */ + border-radius: 8px; + padding: 10px; + min-width: 150px; + background: #f9f9f9; /* Light background */ + font-size: 14px; + color: #3E4739; /* Text color to match the border */ +} + +/* Info box text spacing */ +.info-box div { + margin-bottom: 8px; +} + +/* Button inside the info box */ +.info-box button { + background-color: #D9996B; /* Same background color as route button */ + color: #3E4739; /* Text color */ + padding: 6px 12px; + cursor: pointer; + font-size: 14px; + border-radius: 30px; + border: 2px solid #3E4739; /* Matching border */ + font-weight: 600; + transition: all 0.3s ease; +} + +/* Hover effect for info box button */ +.info-box button:hover { + background-color: #B6CBBC; /* Lighter color on hover */ +} + +/* Disabled button in info box */ +.info-box button:disabled { + background-color: #ddd; + cursor: not-allowed; + border: 2px solid #ccc; +} diff --git a/frontend/sortify/components/map.jsx b/frontend/sortify/components/map.jsx index f54041e6..e4d30b32 100644 --- a/frontend/sortify/components/map.jsx +++ b/frontend/sortify/components/map.jsx @@ -3,10 +3,12 @@ import { useEffect, useRef, useState } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; +import './map.css'; import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; import markerIcon from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; +import {useSearch} from "@/app/context/searchContext"; delete L.Icon.Default.prototype._getIconUrl; @@ -16,7 +18,7 @@ L.Icon.Default.mergeOptions({ shadowUrl: markerShadow.src ?? markerShadow, }); -export default function Map({filter}) { +export default function Map() { // Store references to map and marker instances const mapRef = useRef(null); const markerRef = useRef(null); @@ -27,24 +29,41 @@ export default function Map({filter}) { const routeLayerRef = useRef(null); const [routeVisible, setRouteVisible] = useState(false); + const [routeInfo, setRouteInfo] = useState(null); + + const { search, setSearch } = useSearch(); + const skipNextSearchEffect = useRef(false); + + const [filters, setFilters] = useState({ + "Plast": true, + "Restavfall": false, + "Matavfall": true, + "Papp og papir": false, + "El-avfall": false, + "Klær": false, + "Farlig avfall": false, + "Glass og metall": false, + "Hageavfall": false, + "Brennbart avfall": false + }); - console.log("filter: " + filter) + const toggleFilter = (type) => { + setFilters(prev => ({ + ...prev, + [type]: !prev[type] + })); + }; // Initialize map only once on component mount useEffect(() => { - console.log("Initializing Map") - if (typeof window === 'undefined') return; - const map = L.map('map').setView([60.39, 5.32], 11); mapRef.current = map; - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap', }).addTo(map); - return () => map.remove(); }, []); @@ -55,33 +74,72 @@ export default function Map({filter}) { navigator.geolocation.getCurrentPosition( (position) => { console.log("coords",position.coords) - setUserLocation({ + const newLocation = { lat: position.coords.latitude, lon: position.coords.longitude, - }); + }; + setUserLocation(newLocation); + // Create the user marker immediately with the new location + createUserMarker(mapRef.current, newLocation); }, (error) => console.error("Error getting location:", error.message) ); }, []); - // When both map and location are available, update the map view + // When both map and location are available, mark and zoom onto the users geolocation useEffect(() => { - - console.log("Trying to display locations") - - if (!mapRef.current || !userLocation) { - console.log("Could not find map or userLocation") + if (!mapRef.current || !userLocation) return; + updateUserMarker(mapRef.current, userLocation) + + fetch(`http://localhost:9876/api/locations/sorted?lat=${userLocation.lat}&lon=${userLocation.lon}`) + .then(res => res.json()) + .then(data => { + setLocations(data); + }) + .catch(err => console.error("Failed to fetch locations:", err)); + }, [userLocation]); + + // Update the displayed users location and the displayed locations, if they have changed + useEffect(() => { + if (!mapRef.current || !userLocation || locations.length === 0) return; + + let filtered = [...locations]; // Start with all locations + + // Debug + console.log(locations) + + // Apply search filter if there's a search term + console.log(search) + if (search) { + filtered = filtered.filter(loc => loc.wasteTypes.includes(search)); + const updatedFilters = Object.fromEntries( + Object.keys(filters).map(type => [type, type === search]) + ); + setFilters(updatedFilters); // Update the filters state + setSearch(""); // Reset search after applying + } else { + // Apply checkbox filters + const selected = Object.entries(filters) + .filter(([_, checked]) => checked) + .map(([key]) => key); + if (selected.length) { + filtered = filtered.filter(loc => + loc.wasteTypes.some(type => selected.includes(type)) + ); + } + } + // Check if there are any filtered locations + if (filtered.length === 0) { + // Handle case where no locations match the filters (optional) + setNearestLocation(null); + addLocationMarkers(mapRef.current, []); // Clear any existing markers return; } - updateUserMarker(mapRef.current, userLocation); - fetchAndDisplayLocations( - mapRef.current, - userLocation, - setLocations, - setNearestLocation, - filter - ); - }, [userLocation, filter]); + + // Set the nearest location + setNearestLocation(filtered[0]); + addLocationMarkers(mapRef.current, filtered); + }, [search, filters, locations, userLocation]); // Functionality to toggle the route on or off const toggleRoute = async () => { @@ -101,6 +159,18 @@ export default function Map({filter}) { const routeData = await routeRes.json(); if (routeData.routes?.length) { + + const distanceMeters = routeData.routes[0].distance; + const durationSeconds = routeData.routes[0].duration; + + setRouteInfo({ + distance: (distanceMeters / 1000).toFixed(2), + duration: (durationSeconds / 60).toFixed(2), + }); + + console.log(`Distance to nearest: ${(distanceMeters / 1000).toFixed(2)} km`); + console.log(`ETA: ${(durationSeconds / 60).toFixed(2)} minutes`); + const geoJSON = routeData.routes[0].geometry; const routeLine = L.geoJSON(geoJSON, { style: { color: 'blue', weight: 4 }, @@ -109,6 +179,7 @@ export default function Map({filter}) { routeLayerRef.current = routeLine; setRouteVisible(true); map.fitBounds(routeLine.getBounds()); + } } catch (err) { console.error("Error drawing route:", err); @@ -119,68 +190,143 @@ export default function Map({filter}) { // Map component // Button for toggling the route on or off return ( - <> -
- - +
+
+
+
+
+

Filter

+ + + + + + + + + + + +
+
+ +
+ + + {routeInfo && ( +
+
Distanse: {routeInfo.distance} km
+
Estimert tid: {routeInfo.duration} min
+
+
+ )} +
+
+
); } /** * Places a marker at the user's location and centers the map */ -function updateUserMarker(map, location) { +function createUserMarker(map, location) { if (map._userMarker) { map.removeLayer(map._userMarker); } - const marker = L.marker([location.lat, location.lon]) .addTo(map) .bindPopup("You are here!") .openPopup(); - map._userMarker = marker; map.setView([location.lat, location.lon], 17); } /** - * Fetches nearby locations from the backend, - * highlights the nearest, and displays all with markers. + * Updates the userMarkers location */ -async function fetchAndDisplayLocations(map, userLocation, setLocations, setNearestLocation, filter) { - console.log("Fetching Locations") - try { - const res = await fetch(`http://localhost:9876/api/locations/sorted?lat=${userLocation.lat}&lon=${userLocation.lon}`); - if (!res.ok) throw new Error("Failed to fetch locations"); - - let allLocations = await res.json(); - setLocations(allLocations); - - // Set filter - if(filter) { - console.log("Before filter: " + filter, allLocations) - allLocations = allLocations.filter(location => location.wasteTypes.includes(filter)) - console.log("After filter: " + filter, allLocations) - } - console.log(allLocations) - - if (!allLocations.length) return; - - const nearest = allLocations[0]; - setNearestLocation(nearest); - - - addLocationMarkers(map, allLocations); - //showNearestMarker(map, nearest); - } catch (err) { - console.error("Error fetching locations:", err); +function updateUserMarker(map, location) { + if (map._userMarker) { + map.removeLayer(map._userMarker); } + const marker = L.marker([location.lat, location.lon]) + .addTo(map) + map._userMarker = marker; } /** @@ -189,32 +335,52 @@ async function fetchAndDisplayLocations(map, userLocation, setLocations, setNear let currentMarkerGroup = null; function addLocationMarkers(map, locations) { if (currentMarkerGroup) { - map.removeLayer(currentMarkerGroup) + map.removeLayer(currentMarkerGroup); + } + const markerGroup = L.layerGroup().addTo(map); + currentMarkerGroup = markerGroup; + + // Function to determine color based on waste types + function getMarkerColor(wasteTypes) { + // Remove duplicates by converting to a Set + const uniqueWasteTypes = [...new Set(wasteTypes)]; + + if (uniqueWasteTypes.length > 1) { + // If the location handles multiple waste types, set it to blue + return { color: 'blue', fillColor: 'lightgray' }; + } else if (uniqueWasteTypes.includes("Plast")) { + return { color: 'green', fillColor: 'lightgray' }; + } else if (uniqueWasteTypes.includes("Glass og metall")) { + return { color: 'blue', fillColor: 'lightgray' }; + } else if (uniqueWasteTypes.includes("El-avfall")) { + return { color: 'gray', fillColor: 'lightgray' }; + } else if (uniqueWasteTypes.includes("Papp og papir")) { + return { color: 'brown', fillColor: 'lightgray' }; + } else if (uniqueWasteTypes.includes("Restavfall")) { + return { color: 'red', fillColor: 'lightgray' }; + } else { + return { color: 'purple', fillColor: 'lightgray' }; // Default color + } } - const markerGroup = L.layerGroup().addTo(map) - currentMarkerGroup = markerGroup locations.forEach((loc) => { + // Clean up the wasteTypes by removing duplicates + const uniqueWasteTypes = [...new Set(loc.wasteTypes)]; + + // Determine marker color based on wasteTypes + const { color, fillColor } = getMarkerColor(uniqueWasteTypes); + L.circleMarker([loc.latitude, loc.longitude], { radius: 8, - color: 'blue', - fillColor: '#3f51b5', + color: color, // Border color + fillColor: fillColor, // Fill color fillOpacity: 0.8, }) .addTo(markerGroup) .bindPopup(` - ${loc.name}, ${loc.address}
- ${[...new Set(loc.wasteTypes)].join(", ")} -`); + ${loc.name}, ${loc.address}
+ ${uniqueWasteTypes.join(", ")} + `); }); } -/** - * Highlights the nearest location with a popup marker - */ -function showNearestMarker(map, location) { - L.marker([location.latitude, location.longitude]) - .addTo(map) - .bindPopup(`${location.name}, ${location.address}`) - .openPopup(); -} diff --git a/frontend/sortify/components/searchbar/searchbar.module.css b/frontend/sortify/components/searchbar/searchbar.module.css index 474ebe97..7d4ad863 100644 --- a/frontend/sortify/components/searchbar/searchbar.module.css +++ b/frontend/sortify/components/searchbar/searchbar.module.css @@ -27,4 +27,20 @@ width: 100%; padding: 0 1rem; box-sizing: border-box; -} \ No newline at end of file +} + +.listItem { + padding: 0.5rem 1rem; + cursor: pointer; + border-radius: 6px; + transition: background-color 0.2s ease, font-weight 0.2s ease; +} + +.listItem:hover { + background-color: #D7D1C8; + font-weight: bold; +} + +.listItem:active { + background-color: #BEB7AC; +} diff --git a/frontend/sortify/components/searchbar/searchbar.tsx b/frontend/sortify/components/searchbar/searchbar.tsx index 9895b6f3..648dce50 100644 --- a/frontend/sortify/components/searchbar/searchbar.tsx +++ b/frontend/sortify/components/searchbar/searchbar.tsx @@ -89,7 +89,7 @@ export default function Searcbar(){ setTimeout(()=> setOpen(false), 200)} @@ -105,8 +105,12 @@ export default function Searcbar(){ {queryResult.map((item) => ( - -

setSearch(item.type)}>{item.name}

+ setSearch(item.type)} + > +

{item.name}

))}
diff --git a/frontend/sortify/components/signup/signup.tsx b/frontend/sortify/components/signup/signup.tsx index e74713aa..c34feaa6 100644 --- a/frontend/sortify/components/signup/signup.tsx +++ b/frontend/sortify/components/signup/signup.tsx @@ -20,7 +20,7 @@ export default function SignupModal() { className={styles.signupButton} onClick={handleOpen} > - Sign Up / In + Lag konto / Logg inn - {isSignup ? 'Create an account' : 'Log in to your account'} + {isSignup ? 'Lag en bruker' : 'Logg inn på din bruker'} @@ -45,7 +45,7 @@ export default function SignupModal() {