Files
drp-48/src/routes/space/[id]/+page.svelte
Caspar Jojo Asaam be04f2d869 feat: Implemented Favouriting for a user
Co-Authored-By: Tadios Temesgen <tt2022@ic.ac.uk>
2025-06-13 03:36:16 +01:00

356 lines
9.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import Navbar from "$lib/components/Navbar.svelte";
import crossUrl from "$lib/assets/cross.svg";
import placeholder from "$lib/assets/study_space.png";
import Carousel from "$lib/components/Carousel.svelte";
import CompulsoryTags from "$lib/components/CompulsoryTags.svelte";
import Report from "$lib/components/Report.svelte";
import Feedback from "$lib/components/Feedback.svelte";
import { onMount } from "svelte";
import { gmapsLoader, daysOfWeek, formatTime, type Table } from "$lib";
import Button from "$lib/components/Button.svelte";
import Favourite from "$lib/components/Favourite.svelte";
const { data } = $props();
const { space, supabase, adminMode } = $derived(data);
const place = $derived(space.building_location as google.maps.places.PlaceResult);
const imgUrls = $derived(
space.study_space_images.length === 0
? [placeholder]
: space.study_space_images.map(
(img) =>
supabase.storage.from("files_bucket").getPublicUrl(img.image_path).data
.publicUrl
)
);
let isReportVisible = $state(false);
function hideReport() {
isReportVisible = false;
}
let isFeedbackPromptVisible = $state(false);
function hideFeedbackPrompt() {
isFeedbackPromptVisible = false;
}
let mapElem = $state<HTMLDivElement>();
onMount(async () => {
if (!mapElem) return console.error("Map element not found");
const loader = await gmapsLoader();
const { Map } = await loader.importLibrary("maps");
const { AdvancedMarkerElement } = await loader.importLibrary("marker");
const map = new Map(mapElem, {
center: place.geometry?.location,
zoom: 15,
mapId: "9f4993cd3fb1504d495821a5"
});
new AdvancedMarkerElement({
position: place.geometry?.location,
map
});
});
// Collect all timing entries
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);
onMount(async () => {
const {
data: { session }
} = await supabase.auth.getSession();
if (!session?.user) return;
const { data: fav } = await supabase
.from("favourite_study_spaces")
.select("study_space_id")
.match({ user_id: session.user.id, study_space_id: space.id })
.single();
isFavourite = !!fav;
});
// Toggle a space in/out of favourites
async function handleToggleFavourite() {
const {
data: { session }
} = await supabase.auth.getSession();
if (!session?.user) return;
if (isFavourite) {
await supabase
.from("favourite_study_spaces")
.delete()
.match({ user_id: session.user.id, study_space_id: space.id });
isFavourite = false;
} else {
await supabase
.from("favourite_study_spaces")
.insert([{ user_id: session.user.id, study_space_id: space.id }]);
isFavourite = true;
}
}
</script>
<Navbar>
<a href="/">
<img src={crossUrl} alt="close" />
</a>
</Navbar>
{#if isReportVisible}<Report {data} studySpaceId={space.id} hideFunc={hideReport} />
{/if}
{#if isFeedbackPromptVisible}
<Feedback
studySpaceData={{
...space,
building_location: place
}}
{supabase}
hideFunc={hideFeedbackPrompt}
/>
{/if}
<main>
<Carousel urls={imgUrls} />
<div class="titleContainer">
<div class="nameContainer">{space.location}</div>
<div class="title-fav">
<Favourite {isFavourite} onToggleFavourite={handleToggleFavourite} />
</div>
</div>
{#if space.description != null && space.description.length > 0}
<p class="descContainer">
{space.description}
</p>
<hr />
{/if}
<div class="compulsoryContainer"><CompulsoryTags {space} /></div>
{#if space.tags.length > 0}
<div class="tagContainer">
{#each space.tags as tag (tag)}
<span class="tag">
{tag}
</span>
{/each}
</div>
{/if}
<hr />
<div class="subtitle">Opening Times:</div>
{#each Array(7).keys() as idx (idx)}
{@const entries = timingsPerDay[idx]}
<div class="opening-entry">
<span class="day">{daysOfWeek[idx]}</span>
<div class="times">
{#each entries as entry (entry)}
<span class="time">
{entry.open_today_status
? "Open All Day"
: entry.open_today_status === false
? "Closed"
: `${formatTime(entry.opens_at)} ${formatTime(entry.closes_at)}`}
</span>
{:else}
<span class="time">Not known</span>
{/each}
</div>
</div>
{/each}
<hr />
<div class="subtitle">Where it is:</div>
<p class="addrContainer">
{#if place.name}
{place.name} <br />
{/if}
{#each place.formatted_address?.split(",") || [] as line (line)}
{line.trim()} <br />
{/each}
</p>
<div class="addrMap" bind:this={mapElem}></div>
<button
type="button"
class="feedbackButton"
onclick={() => {
isFeedbackPromptVisible = true;
}}
>
Help categorise this space
</button>
<div class="actions">
{#if adminMode}
<Button href="/space/{space.id}/edit" type="link">Edit</Button>
{:else}
<Button onclick={() => (isReportVisible = true)} style="red">Report</Button>
{/if}
</div>
</main>
<style>
main {
display: flex;
flex-direction: column;
padding: 0 0 1rem 0;
max-width: 32rem;
margin: 0 auto;
}
img {
display: block;
width: 100%;
aspect-ratio: 1/0.8;
object-fit: cover;
object-position: center;
}
hr {
height: 2px;
background-color: #2e3c42;
width: 70%;
border: none;
margin: 1rem auto 0;
}
.nameContainer {
z-index: 10;
display: block;
width: 100%;
padding: 0.6rem;
margin-top: -0.5rem;
object-position: center;
background-color: #49bd85;
border-radius: 8px;
font-size: 2.8rem;
font-weight: bold;
color: #ffffff;
}
.descContainer {
display: block;
width: 100%;
padding: 1.4rem;
margin-top: -0.5rem;
object-position: center;
border-radius: 8px;
font-size: 1.2rem;
}
.subtitle {
font-size: 1.2rem;
font-weight: bold;
color: #ffffff;
padding: 1.4rem;
}
.addrContainer {
font-size: 1.2rem;
padding: 0rem 1.4rem 1rem;
}
.tagContainer {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 1.4rem;
border-radius: 0.5rem;
background: none;
}
.tag {
display: flex;
align-items: center;
border-radius: 0.25rem;
background-color: #2e4653;
color: #eaffeb;
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.6rem;
}
.compulsoryContainer {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
padding: 1.4rem;
font-size: 1.3rem;
padding-bottom: 0;
}
.addrMap {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 0.5rem;
border: 2px solid #eaffeb;
}
.feedbackButton {
width: 100%;
padding: 0.7rem;
border-radius: 0.5rem;
border: none;
background-color: #49bd85;
color: #ffffff;
font-size: 1rem;
cursor: pointer;
margin-top: 1rem;
text-align: center;
}
.actions {
display: flex;
flex-direction: column;
padding-top: 1rem;
}
.opening-entry {
display: flex;
gap: 0.75rem;
padding: 0.5rem 1.4rem;
align-items: center;
background-color: #2e4653;
margin: 0.2rem;
border-radius: 0.5rem;
}
.opening-entry .day {
font-weight: bold;
color: #ffffff;
align-items: center;
justify-content: center;
}
.opening-entry .times {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 0.25rem 1.5rem;
flex: 1;
align-items: end;
}
.opening-entry .time {
font-family: monospace;
color: #eaffeb;
}
.titleContainer {
display: flex;
align-items: center;
justify-content: space-between;
margin: -0.5rem 0 1rem;
}
.title-fav {
font-size: 2rem;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
padding: 0.25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.7);
color: red;
}
</style>