feat: sort by location
This commit is contained in:
@@ -66,3 +66,20 @@ export function timeToMins(time: string) {
|
|||||||
const [hour, min] = time.split(":");
|
const [hour, min] = time.split(":");
|
||||||
return Number(hour) * 60 + Number(min);
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,16 @@
|
|||||||
import defaultImg from "$lib/assets/study_space.png";
|
import defaultImg from "$lib/assets/study_space.png";
|
||||||
import crossUrl from "$lib/assets/cross.svg";
|
import crossUrl from "$lib/assets/cross.svg";
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
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 Button from "$lib/components/Button.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
const {
|
const {
|
||||||
@@ -137,6 +145,60 @@
|
|||||||
tagFilter = "";
|
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>
|
</script>
|
||||||
|
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@@ -169,7 +231,7 @@
|
|||||||
<div class="tag-filter-container">
|
<div class="tag-filter-container">
|
||||||
<form>
|
<form>
|
||||||
<div class="tagDisplay">
|
<div class="tagDisplay">
|
||||||
{#each selectedTags as tagName (tagName)}
|
{#each selectedTags as tagName, idx (tagName + idx)}
|
||||||
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
<button class="tag" onclick={deleteTag(tagName)} type="button">
|
||||||
{tagName}
|
{tagName}
|
||||||
<img src={crossUrl} alt="delete" /></button
|
<img src={crossUrl} alt="delete" /></button
|
||||||
@@ -198,7 +260,7 @@
|
|||||||
/>
|
/>
|
||||||
{#if dropdownVisible}
|
{#if dropdownVisible}
|
||||||
<div class="tagDropdown">
|
<div class="tagDropdown">
|
||||||
{#each filteredTags as avaliableTag (avaliableTag)}
|
{#each filteredTags as avaliableTag, idx (avaliableTag + idx)}
|
||||||
<button
|
<button
|
||||||
class="avaliableTag"
|
class="avaliableTag"
|
||||||
onclick={addTag(avaliableTag)}
|
onclick={addTag(avaliableTag)}
|
||||||
@@ -217,8 +279,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 =
|
{@const imgUrl =
|
||||||
studySpace.study_space_images.length > 0
|
studySpace.study_space_images.length > 0
|
||||||
? supabase.storage
|
? supabase.storage
|
||||||
@@ -284,6 +363,13 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 0.5rem;
|
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 {
|
.time-filter-container label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -403,6 +489,11 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sortMap {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 20rem) {
|
@media (max-width: 20rem) {
|
||||||
main {
|
main {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
|
||||||
{#if space.tags.length > 0}
|
{#if space.tags.length > 0}
|
||||||
<div class="tagContainer">
|
<div class="tagContainer">
|
||||||
{#each space.tags as tag (tag)}
|
{#each space.tags as tag, idx (tag + idx)}
|
||||||
<span class="tag">
|
<span class="tag">
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
{#if place.name}
|
{#if place.name}
|
||||||
{place.name} <br />
|
{place.name} <br />
|
||||||
{/if}
|
{/if}
|
||||||
{#each place.formatted_address?.split(",") || [] as line (line)}
|
{#each place.formatted_address?.split(",") || [] as line, idx (line + idx)}
|
||||||
{line.trim()} <br />
|
{line.trim()} <br />
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<label for="openingTimes">Opening times (Optional):</label>
|
<label for="openingTimes">Opening times (Optional):</label>
|
||||||
<div class="allDaysTiming">
|
<div class="allDaysTiming">
|
||||||
{#each studySpaceData.opening_times as opening_time, index (index)}
|
{#each studySpaceData.opening_times as opening_time, index (opening_time)}
|
||||||
<OpeningTimesDay
|
<OpeningTimesDay
|
||||||
{index}
|
{index}
|
||||||
bind:openingValue={opening_time.opens_at}
|
bind:openingValue={opening_time.opens_at}
|
||||||
|
|||||||
Reference in New Issue
Block a user