282 lines
7.2 KiB
Svelte
282 lines
7.2 KiB
Svelte
<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 } from "$lib";
|
||
import Button from "$lib/components/Button.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
|
||
});
|
||
});
|
||
|
||
const hoursByDay = $derived(new Map(space.study_space_hours.map((h) => [h.day_of_week, h])));
|
||
|
||
const openingEntries = daysOfWeek.map((day, idx) => ({
|
||
day,
|
||
entry: hoursByDay.get(idx)
|
||
}));
|
||
</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="nameContainer">
|
||
{space.location}
|
||
</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 openingEntries as { day, entry } (entry)}
|
||
<div class="opening-entry">
|
||
<span class="day">{day}:</span>
|
||
<span class="times">
|
||
{#if entry}
|
||
{entry.is_24_7
|
||
? "Open All Day"
|
||
: `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`}
|
||
{:else}
|
||
Closed
|
||
{/if}
|
||
</span>
|
||
</div>
|
||
{/each}
|
||
|
||
<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: 0 auto;
|
||
}
|
||
|
||
.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: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 0.75rem;
|
||
padding: 0.5rem 1.4rem;
|
||
align-items: center;
|
||
}
|
||
.opening-entry .day {
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
white-space: nowrap;
|
||
}
|
||
.opening-entry .times {
|
||
font-family: monospace;
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
color: #eaffeb;
|
||
}
|
||
</style>
|