Merge branch 'feat/open-now' into 'master'

Feat/open now

See merge request gk1623/drp-48!25

Co-authored-by: Barf-Vader <47476490+Barf-Vader@users.noreply.github.com>
This commit is contained in:
Ling, Alex
2025-06-13 13:22:42 +00:00
5 changed files with 117 additions and 38 deletions

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import CompulsoryTags from "./CompulsoryTags.svelte"; import CompulsoryTags from "./CompulsoryTags.svelte";
import OpeningTimes from "./OpeningTimes.svelte";
import Favourite from "./Favourite.svelte"; import Favourite from "./Favourite.svelte";
import type { Table } from "$lib"; import type { Table } from "$lib";
interface Props { interface Props {
space: Table<"study_spaces">; space: Table<"study_spaces">;
hours: Table<"study_space_hours">[];
alt: string; alt: string;
imgSrc: string; imgSrc: string;
href?: string; href?: string;
isFavourite: boolean; isFavourite: boolean;
isAvailable?: boolean;
onToggleFavourite?: () => void; onToggleFavourite?: () => void;
footer?: string;
} }
const { space, hours, alt, imgSrc, href, isFavourite, onToggleFavourite }: Props = $props(); const { space, alt, imgSrc, href, isFavourite, onToggleFavourite, isAvailable, footer }: Props =
$props();
</script> </script>
<a class="card" {href}> <a class="card {isAvailable ? 'green' : 'grey'}" {href}>
<!-- <img src={imgSrc} {alt} /> --> <!-- <img src={imgSrc} {alt} /> -->
<div class="image-container"> <div class="image-container">
<img src={imgSrc} {alt} /> <img src={imgSrc} {alt} />
@@ -34,11 +34,14 @@
{#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 (tag)}
<span class="tag">{tag}</span> <span class="tag {isAvailable ? 'tagGreen' : 'tagGrey'}">{tag}</span>
{/each} {/each}
</div> </div>
{/if} {/if}
<div class="openingTimesContainer"><OpeningTimes {hours} /></div> <div class="spacer"></div>
{#if footer}
<div class="footer">{footer}</div>
{/if}
</div> </div>
</a> </a>
@@ -46,14 +49,25 @@
.card { .card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #49bd85;
width: 100%; width: 100%;
max-width: 20rem; max-width: 20rem;
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
} }
.green {
background-color: #49bd85;
}
.grey {
background-color: #2e4653;
}
.spacer {
flex: 1;
}
.description { .description {
flex: 1;
display: flex;
flex-direction: column;
padding: 0.5rem; padding: 0.5rem;
color: #edebe9; color: #edebe9;
} }
@@ -83,13 +97,20 @@
justify-content: center; justify-content: center;
text-align: center; text-align: center;
border-radius: 0.25rem; border-radius: 0.25rem;
background-color: #2e4653;
color: #eaffeb; color: #eaffeb;
font-size: 0.875rem; font-size: 0.875rem;
cursor: pointer; cursor: pointer;
padding: 0.2rem 0.6rem; padding: 0.2rem 0.6rem;
} }
.tagGreen {
background-color: #2e4653;
}
.tagGrey {
background-color: #49bd85;
}
.compulsoryContainer { .compulsoryContainer {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(3.75rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(3.75rem, 1fr));
@@ -109,4 +130,16 @@
padding: 0.25rem; padding: 0.25rem;
z-index: 1; z-index: 1;
} }
.footer {
width: 100%;
color: #2e4653;
background-color: #eaffeb;
align-self: flex-end;
border-radius: 0.5rem;
padding: 0.1rem;
text-align: center;
font-weight: bold;
margin-top: 0.4rem;
}
</style> </style>

View File

@@ -84,3 +84,21 @@ export function haversineDistance(
Math.pow(Math.sin(deltaLng / 2), 2) * Math.cos(lat1) * Math.cos(lat2); Math.pow(Math.sin(deltaLng / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
return radius * 2 * Math.asin(Math.sqrt(e1)); return radius * 2 * Math.asin(Math.sqrt(e1));
} }
export function collectTimings(study_space_hours: Table<"study_space_hours">[]) {
// Collect all timing entries
const timingsPerDay: Record<number, Table<"study_space_hours">[]> = {
0: [],
1: [],
2: [],
3: [],
4: [],
5: [],
6: []
};
for (const entry of study_space_hours) {
timingsPerDay[entry.day_of_week].push(entry);
}
return timingsPerDay;
}

View File

@@ -4,10 +4,11 @@
import crossUrl from "$lib/assets/cross.svg"; import crossUrl from "$lib/assets/cross.svg";
import searchUrl from "$lib/assets/search.svg"; import searchUrl from "$lib/assets/search.svg";
import Navbar from "$lib/components/Navbar.svelte"; import Navbar from "$lib/components/Navbar.svelte";
import { haversineDistance, timeToMins } from "$lib"; import { collectTimings, timeToMins, haversineDistance } from "$lib";
import Button from "$lib/components/Button.svelte"; import Button from "$lib/components/Button.svelte";
import { urldecodeSortFilter } from "$lib/filter.js"; import { urldecodeSortFilter } from "$lib/filter.js";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import type { Table } from "$lib";
const { data } = $props(); const { data } = $props();
const { const {
@@ -123,6 +124,45 @@
}) })
: filteredStudySpaces : filteredStudySpaces
); );
// Open now
function isOpenNow(all_study_space_hours: Table<"study_space_hours">[]) {
const now = new Date();
const time = now.toTimeString().slice(0, 5);
const day = now.getDay();
const timingsPerDay = collectTimings(all_study_space_hours);
for (const timing of timingsPerDay[day]) {
if (timing.open_today_status === true) {
return { isOpen: true, message: `Open all day` };
} else if (timing.open_today_status === false) {
return { isOpen: false, message: `Closed today` };
} else {
const opensFor = timeUntilClosing(timing.opens_at, timing.closes_at, time);
if (opensFor) {
return {
isOpen: true,
message: `Open now for: ${minsToReadableHours(timeToMins(time))}`
};
}
}
}
return { isOpen: false, message: "Closed right now" };
}
function timeUntilClosing(openingTime: string, closingTime: string, currentTime: string) {
const currTimeInMins = timeToMins(currentTime);
const OpeningTimeInMins = timeToMins(openingTime);
const closingTimeInMins = timeToMins(closingTime);
if (currTimeInMins >= OpeningTimeInMins && currTimeInMins < closingTimeInMins) {
return closingTimeInMins - currTimeInMins;
}
}
function minsToReadableHours(mins: number) {
return `${Math.floor(mins / 60)} hrs, ${mins % 60} mins`;
}
</script> </script>
<Navbar> <Navbar>
@@ -161,9 +201,14 @@
href="/space/{studySpace.id}" href="/space/{studySpace.id}"
imgSrc={imgUrl} imgSrc={imgUrl}
space={studySpace} space={studySpace}
hours={studySpace.study_space_hours}
isFavourite={favouriteIds.includes(studySpace.id)} isFavourite={favouriteIds.includes(studySpace.id)}
onToggleFavourite={session ? () => handleToggleFavourite(studySpace.id) : undefined} onToggleFavourite={session ? () => handleToggleFavourite(studySpace.id) : undefined}
isAvailable={studySpace.study_space_hours.length === 0
? undefined
: isOpenNow(studySpace.study_space_hours).isOpen}
footer={studySpace.study_space_hours.length === 0
? undefined
: isOpenNow(studySpace.study_space_hours).message}
/> />
{/each} {/each}
</main> </main>

View File

@@ -7,7 +7,7 @@
import Report from "$lib/components/Report.svelte"; import Report from "$lib/components/Report.svelte";
import Feedback from "$lib/components/Feedback.svelte"; import Feedback from "$lib/components/Feedback.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { gmapsLoader, daysOfWeek, formatTime, type Table } from "$lib"; import { gmapsLoader, daysOfWeek, formatTime, collectTimings } from "$lib";
import Button from "$lib/components/Button.svelte"; import Button from "$lib/components/Button.svelte";
import Favourite from "$lib/components/Favourite.svelte"; import Favourite from "$lib/components/Favourite.svelte";
@@ -52,20 +52,7 @@
}); });
}); });
// Collect all timing entries let timingsPerDay = collectTimings(space.study_space_hours);
let timingsPerDay: Record<number, Table<"study_space_hours">[]> = {
0: [],
1: [],
2: [],
3: [],
4: [],
5: [],
6: []
};
for (const entry of space.study_space_hours) {
timingsPerDay[entry.day_of_week].push(entry);
}
let isFavourite = $state(false); let isFavourite = $state(false);
onMount(async () => { onMount(async () => {

View File

@@ -59,7 +59,6 @@
let spaceImgs = $state<FileList>(); let spaceImgs = $state<FileList>();
let uploading = $state(false); let uploading = $state(false);
function checkTimings() { function checkTimings() {
console.log(studySpaceData.opening_times);
let cannotExist = [] as number[]; let cannotExist = [] as number[];
const opensAtMinsAll = timeToMins(allDays.opens_at); const opensAtMinsAll = timeToMins(allDays.opens_at);
@@ -91,7 +90,6 @@
function genTimings(studySpaceId: string) { function genTimings(studySpaceId: string) {
const fullDayOfWeek = [0, 1, 2, 3, 4, 5, 6]; const fullDayOfWeek = [0, 1, 2, 3, 4, 5, 6];
console.log(studySpaceData.opening_times, allDays);
// all day only // all day only
if ( if (
studySpaceData.opening_times.length === 0 && studySpaceData.opening_times.length === 0 &&
@@ -101,8 +99,8 @@
return fullDayOfWeek.map((day) => ({ return fullDayOfWeek.map((day) => ({
study_space_id: studySpaceId, study_space_id: studySpaceId,
day_of_week: day, day_of_week: day,
opens_at: allDays.open_today_status == null ? allDays.opens_at : "00:00", opens_at: allDays.open_today_status !== null ? allDays.opens_at : "00:00",
closes_at: allDays.open_today_status == null ? allDays.closes_at : "00:00", closes_at: allDays.open_today_status !== null ? allDays.closes_at : "00:01",
open_today_status: allDays.open_today_status open_today_status: allDays.open_today_status
})); }));
} }
@@ -114,16 +112,16 @@
.map((h) => ({ .map((h) => ({
study_space_id: studySpaceId, study_space_id: studySpaceId,
day_of_week: h.day_of_week, day_of_week: h.day_of_week,
opens_at: h.opens_at, opens_at: h.open_today_status === null ? h.opens_at : "00:00",
closes_at: h.closes_at, closes_at: h.open_today_status === null ? h.closes_at : "00:01",
open_today_status: h.open_today_status open_today_status: h.open_today_status
})) }))
.concat( .concat(
nonDefinedDays.map((day) => ({ nonDefinedDays.map((day) => ({
study_space_id: studySpaceId, study_space_id: studySpaceId,
day_of_week: day, day_of_week: day,
opens_at: allDays.opens_at, opens_at: allDays.open_today_status === null ? allDays.opens_at : "00:00",
closes_at: allDays.closes_at, closes_at: allDays.open_today_status === null ? allDays.closes_at : "00:01",
open_today_status: allDays.open_today_status open_today_status: allDays.open_today_status
})) }))
); );
@@ -134,10 +132,8 @@
if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file."); if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file.");
if (!studySpaceData.building_location) return alert("Please select a building location."); if (!studySpaceData.building_location) return alert("Please select a building location.");
const { opening_times, ...spacePayload } = studySpaceData; // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { opening_times: _, ...spacePayload } = studySpaceData;
// explicitly mark opening_times as used to avoid unused warning
void opening_times;
const { data: studySpaceInsert, error: studySpaceError } = await supabase const { data: studySpaceInsert, error: studySpaceError } = await supabase
.from("study_spaces") .from("study_spaces")