Files
drp-48/src/routes/space/[id]/+page.svelte
2025-06-16 21:31:34 +01:00

359 lines
9.6 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, collectTimings } 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
});
});
let timingsPerDay = collectTimings(space.study_space_hours);
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>
<div class="imgContainer">
{#await supabase.auth.getSession() then resp}
{#if resp.data.session}
<div class="title-fav">
<Favourite
{isFavourite}
onToggleFavourite={handleToggleFavourite}
imgSize={27}
/>
</div>
{/if}
{/await}
<Carousel urls={imgUrls} />
</div>
<div class="nameContainer">
<div class="locationContainer">{space.location}</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, idx (tag + idx)}
<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">Directions:</div>
<p class="addrContainer">
{space.directions}
</p>
<div class="subtitle">Where it is:</div>
<p class="addrContainer">
{#if place.name}
{place.name} <br />
{/if}
{#each place.formatted_address?.split(",") || [] as line, idx (line + idx)}
{line.trim()} <br />
{/each}
</p>
<div class="addrMap" bind:this={mapElem}></div>
<button
type="button"
class="feedbackButton"
onclick={() => {
isFeedbackPromptVisible = true;
}}
>
Update Tags
</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 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
padding: 0.6rem;
margin-top: -0.5rem;
background-color: #189f5e;
border-radius: 8px;
font-size: 2.8rem;
font-weight: bold;
color: #ffffff;
z-index: 1;
}
.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: #189f5e;
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;
}
.imgContainer {
position: relative;
}
.title-fav {
position: absolute;
top: 0;
right: 0;
background: #189f5e;
border-radius: 0 0 0 0.5rem;
z-index: 1;
width: 3.75rem;
height: 3.75rem;
}
</style>