feat: sort by location

This commit is contained in:
2025-06-12 23:50:24 +01:00
parent ee190d90db
commit ce6c391d81
4 changed files with 115 additions and 7 deletions

View File

@@ -66,3 +66,20 @@ export function timeToMins(time: string) {
const [hour, min] = time.split(":");
return Number(hour) * 60 + Number(min);
}
export function haversineDistance(
lat1Deg: number,
lng1Deg: number,
lat2Deg: number,
lng2Deg: number,
radius: number = 6371e3
): number {
const lat1 = lat1Deg * (Math.PI / 180);
const lat2 = lat2Deg * (Math.PI / 180);
const deltaLat = (lat2Deg - lat1Deg) * (Math.PI / 180);
const deltaLng = (lng2Deg - lng1Deg) * (Math.PI / 180);
const e1 =
Math.pow(Math.sin(deltaLat / 2), 2) +
Math.pow(Math.sin(deltaLng / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
return radius * 2 * Math.asin(Math.sqrt(e1));
}

View File

@@ -3,8 +3,16 @@
import defaultImg from "$lib/assets/study_space.png";
import crossUrl from "$lib/assets/cross.svg";
import Navbar from "$lib/components/Navbar.svelte";
import { allTags, volumeTags, wifiTags, powerOutletTags } from "$lib";
import {
allTags,
volumeTags,
wifiTags,
powerOutletTags,
gmapsLoader,
haversineDistance
} from "$lib";
import Button from "$lib/components/Button.svelte";
import { onMount } from "svelte";
const { data } = $props();
const {
@@ -137,6 +145,60 @@
tagFilter = "";
};
}
let sortMapElem = $state<HTMLDivElement>();
let sortNear = $state<{ lat: number; lng: number }>();
const sortedStudySpaces = $derived(
sortNear
? studySpaces.toSorted((a, b) => {
if (!sortNear) return 0;
type DBLatLng = { lat: number; lng: number } | undefined;
const aLoc = a.building_location as unknown as google.maps.places.PlaceResult;
const bLoc = b.building_location as unknown as google.maps.places.PlaceResult;
const aLatLng = aLoc.geometry?.location as DBLatLng;
const bLatLng = bLoc.geometry?.location as DBLatLng;
const aDistance = haversineDistance(
sortNear.lat,
sortNear.lng,
aLatLng?.lat || sortNear.lat,
aLatLng?.lng || sortNear.lng
);
const bDistance = haversineDistance(
sortNear.lat,
sortNear.lng,
bLatLng?.lat || sortNear.lat,
bLatLng?.lng || sortNear.lng
);
return aDistance - bDistance;
})
: filteredStudySpaces
);
let marker: google.maps.marker.AdvancedMarkerElement | undefined;
onMount(async () => {
if (!sortMapElem) return console.error("sortMapElem is not defined");
const loader = await gmapsLoader();
const { Map } = await loader.importLibrary("maps");
const { AdvancedMarkerElement } = await loader.importLibrary("marker");
const map = new Map(sortMapElem, {
center: { lat: 53.6, lng: -1.56 },
zoom: 5,
mapId: "9f4993cd3fb1504d495821a5"
});
marker = new AdvancedMarkerElement({
map,
title: "Find near here"
});
map.addListener("click", (e: google.maps.MapMouseEvent) => {
console.log("Clicked map at", e.latLng);
sortNear = e.latLng
? {
lat: e.latLng.lat(),
lng: e.latLng.lng()
}
: sortNear;
if (marker) marker.position = e.latLng;
});
});
</script>
<Navbar>
@@ -169,7 +231,7 @@
<div class="tag-filter-container">
<form>
<div class="tagDisplay">
{#each selectedTags as tagName (tagName)}
{#each selectedTags as tagName, idx (tagName + idx)}
<button class="tag" onclick={deleteTag(tagName)} type="button">
{tagName}
<img src={crossUrl} alt="delete" /></button
@@ -198,7 +260,7 @@
/>
{#if dropdownVisible}
<div class="tagDropdown">
{#each filteredTags as avaliableTag (avaliableTag)}
{#each filteredTags as avaliableTag, idx (avaliableTag + idx)}
<button
class="avaliableTag"
onclick={addTag(avaliableTag)}
@@ -217,8 +279,25 @@
</div>
</form>
</div>
<div class="location-filter-container">
<h3>Find nearby:</h3>
<Button
onclick={() => {
navigator.geolocation.getCurrentPosition((position) => {
if (marker)
sortNear = marker.position = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
});
}}
>
Use current location
</Button>
<div class="sortMap" bind:this={sortMapElem}></div>
</div>
{#each filteredStudySpaces as studySpace (studySpace.id)}
{#each sortedStudySpaces as studySpace (studySpace.id)}
{@const imgUrl =
studySpace.study_space_images.length > 0
? supabase.storage
@@ -284,6 +363,13 @@
justify-content: center;
margin-bottom: 0.5rem;
}
.location-filter-container {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.time-filter-container label {
display: flex;
align-items: center;
@@ -403,6 +489,11 @@
text-decoration: underline;
}
.sortMap {
aspect-ratio: 1 / 1;
width: 100%;
}
@media (max-width: 20rem) {
main {
grid-template-columns: 1fr;

View File

@@ -137,7 +137,7 @@
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
{#if space.tags.length > 0}
<div class="tagContainer">
{#each space.tags as tag (tag)}
{#each space.tags as tag, idx (tag + idx)}
<span class="tag">
{tag}
</span>
@@ -172,7 +172,7 @@
{#if place.name}
{place.name} <br />
{/if}
{#each place.formatted_address?.split(",") || [] as line (line)}
{#each place.formatted_address?.split(",") || [] as line, idx (line + idx)}
{line.trim()} <br />
{/each}
</p>

View File

@@ -370,7 +370,7 @@
</div>
<label for="openingTimes">Opening times (Optional):</label>
<div class="allDaysTiming">
{#each studySpaceData.opening_times as opening_time, index (index)}
{#each studySpaceData.opening_times as opening_time, index (opening_time)}
<OpeningTimesDay
{index}
bind:openingValue={opening_time.opens_at}