Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/sortify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ bun dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/mainContent.tsx`. The page auto-updates as you edit the file.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

Expand Down
25 changes: 25 additions & 0 deletions frontend/sortify/app/context/searchContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';
import { createContext, useState, useContext, ReactNode } from 'react';

type SearchContextType = {
search: string;
setSearch: (value: string) => void;
};

const SearchContext = createContext<SearchContextType | undefined>(undefined);

export const SearchProvider = ({ children }: { children: ReactNode }) => {
const [search, setSearch] = useState('');

return (
<SearchContext.Provider value={{ search, setSearch }}>
{children}
</SearchContext.Provider>
);
};

export const useSearch = () => {
const context = useContext(SearchContext);
if (!context) throw new Error('useSearch must be used within a SearchProvider');
return context;
};
15 changes: 8 additions & 7 deletions frontend/sortify/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "./component/header/header"
import Header from "@/components/header/header"
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import Footer from "./component/footer/footer";
import MainContent from "../app/mainContent";
import Footer from "@/components/footer/footer";
import Page from "./page";
import styles from "./layout.module.css"
import { SearchProvider } from "@/app/context/searchContext";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -34,10 +35,10 @@ export default function RootLayout({
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<AppRouterCacheProvider>
<div className={styles.pageWrapper}>
<Header/>
<main className={styles.mainWrapper}>
<MainContent/>
</main>
<SearchProvider>
<Header/>
{children}
</SearchProvider>
<Footer/>
</div>
</AppRouterCacheProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import dynamic from "next/dynamic";
import styles from './mainContent.module.css';
const Map = dynamic(() => import("./component/map"), {ssr: false});
const Map = dynamic(() => import('../components/map'), {ssr: false});
import { useSearch } from "@/app/context/searchContext";

export default function MainContent(){
export default function Page(){
const { search } = useSearch();
return(
<main className={styles.mainContent}>
<h1 className={styles.title}>Recycling map</h1>
<Map/>
<Map filter={search}/>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styles from "./header.module.css";
import Searchbar from "../searchbar/searchbar";
import SignupModal from "@/app/component/signup/signup";
import DropdownMenu from "@/app/component/dropDownMeny/dropDownMenu";
import SignupModal from "@/components/signup/signup";
import DropdownMenu from "@/components/dropDownMeny/dropDownMenu";

export default function Header() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ L.Icon.Default.mergeOptions({
shadowUrl: markerShadow.src ?? markerShadow,
});

export default function Map() {
export default function Map({filter}) {
// Store references to map and marker instances
const mapRef = useRef(null);
const markerRef = useRef(null);
Expand All @@ -28,8 +28,13 @@ export default function Map() {
const routeLayerRef = useRef(null);
const [routeVisible, setRouteVisible] = useState(false);

console.log("filter: " + filter)

// 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);
Expand All @@ -45,8 +50,11 @@ export default function Map() {

// Fetch user's geolocation when component mounts
useEffect(() => {

console.log("Finding current position");
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("coords",position.coords)
setUserLocation({
lat: position.coords.latitude,
lon: position.coords.longitude,
Expand All @@ -58,10 +66,22 @@ export default function Map() {

// When both map and location are available, update the map view
useEffect(() => {
if (!mapRef.current || !userLocation) return;

console.log("Trying to display locations")

if (!mapRef.current || !userLocation) {
console.log("Could not find map or userLocation")
return;
}
updateUserMarker(mapRef.current, userLocation);
fetchAndDisplayLocations(mapRef.current, userLocation, setLocations, setNearestLocation);
}, [userLocation]);
fetchAndDisplayLocations(
mapRef.current,
userLocation,
setLocations,
setNearestLocation,
filter
);
}, [userLocation, filter]);

// Functionality to toggle the route on or off
const toggleRoute = async () => {
Expand Down Expand Up @@ -133,21 +153,31 @@ function updateUserMarker(map, location) {
* Fetches nearby locations from the backend,
* highlights the nearest, and displays all with markers.
*/
async function fetchAndDisplayLocations(map, userLocation, setLocations, setNearestLocation) {
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");

const allLocations = await res.json();
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);
//showNearestMarker(map, nearest);
} catch (err) {
console.error("Error fetching locations:", err);
}
Expand All @@ -156,15 +186,22 @@ async function fetchAndDisplayLocations(map, userLocation, setLocations, setNear
/**
* Adds circular markers for all nearby locations
*/
let currentMarkerGroup = null;
function addLocationMarkers(map, locations) {
if (currentMarkerGroup) {
map.removeLayer(currentMarkerGroup)
}
const markerGroup = L.layerGroup().addTo(map)
currentMarkerGroup = markerGroup

locations.forEach((loc) => {
L.circleMarker([loc.latitude, loc.longitude], {
radius: 8,
color: 'blue',
fillColor: '#3f51b5',
fillOpacity: 0.8,
})
.addTo(map)
.addTo(markerGroup)
.bindPopup(`
<b>${loc.name}, ${loc.address}</b><br>
${[...new Set(loc.wasteTypes)].join(", ")}
Expand All @@ -180,4 +217,4 @@ function showNearestMarker(map, location) {
.addTo(map)
.bindPopup(`${location.name}, ${location.address}`)
.openPopup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { InputAdornment, OutlinedInput, Popper, Paper, List, ListItem, ClickAway
import SearchIcon from "@mui/icons-material/Search"
import styles from "./searchbar.module.css"
import { UUID } from "crypto";
import { useSearch } from "@/app/context/searchContext";


type wasteItem = {
id: UUID,
Expand All @@ -15,6 +17,8 @@ type wasteItem = {

export default function Searcbar(){

const { search, setSearch } = useSearch()

const [wasteItems, setWasteItems] = useState<wasteItem[]>([]);

useEffect(() => {
Expand All @@ -35,8 +39,8 @@ export default function Searcbar(){


const [query, setQuery] = useState("")
const itemList = wasteItems.map((item) => item.name);
const [queryResult, setQueryResult] = useState<string[]>([])
//const itemList = wasteItems.map((item) => item.name);
const [queryResult, setQueryResult] = useState<wasteItem[]>([])
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);

Expand All @@ -48,14 +52,14 @@ export default function Searcbar(){
console.log("Query updated:", newQuery);
};

function searchResults(query: String): string[]{
function searchResults(query: String): wasteItem[]{
if (query.length === 0) return [];
const sortedResults = new Map();
const input = query.toLowerCase();
let score = 1000;
for (let i = 0; i < itemList.length; i++) {
if (itemList[i].length < input.length-2) continue;
const test = itemList[i].toLowerCase();
for (let i = 0; i < wasteItems.length; i++) {
if (wasteItems[i].name.length < input.length-2) continue;
const test = wasteItems[i].name.toLowerCase();

if (test === input) {
score = 0; // Eksakt match (best mulig treff)
Expand All @@ -66,21 +70,20 @@ export default function Searcbar(){
} else {
score = 3 + distance(input, test); // Fuzzy match
}
sortedResults.set(itemList[i], score)
sortedResults.set(wasteItems[i], score)

}

return getSmallestKeys(sortedResults)
};

function getSmallestKeys(map: Map<string, number>, count = 5): string[] {
function getSmallestKeys(map: Map<wasteItem, number>, count = 5): wasteItem[] {
return [...map.entries()]
.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length) // Sort by values in ascending order
.sort((a, b) => a[1] - b[1] || a[0].name.length - b[0].name.length) // Sort by values in ascending order
.slice(0, count) // Get the first `count` entries
.map(([key]) => key); // Extract and return sorted keys
};


return (
<div className={styles.searchbarWrapper}>
<OutlinedInput
Expand All @@ -102,8 +105,8 @@ export default function Searcbar(){
<Paper>
<List>
{queryResult.map((item) => (
<ListItem key={item}>
{item}
<ListItem key={item.name}>
<p onClick={() => setSearch(item.type)}>{item.name}</p>
</ListItem>
))}
</List>
Expand Down
Loading