feat: initial uploads and single study space view

Co-Authored-By: Alex Ling <al443@ic.ac.uk>
This commit is contained in:
2025-06-04 18:10:45 +01:00
parent b02f2b2303
commit 40435df5e2
16 changed files with 486 additions and 86 deletions

View File

@@ -13,7 +13,22 @@
<style>
:global(body) {
margin: 0;
padding: 0;
width: 100vw;
}
:global(html) {
background-color: #182125;
color: #eaffeb;
}
:global(*) {
box-sizing: border-box;
font-family: Inter;
}
:global(h1, h2, h3, h4, h5, h6, p) {
margin: 0;
padding: 0;
}

View File

@@ -3,57 +3,19 @@
import defaultImg from "$lib/assets/study_space.png";
import { invalidate } from "$app/navigation";
import type { Table } from "$lib";
import crossUrl from "$lib/assets/cross.svg";
import Navbar from "$lib/components/Navbar.svelte";
const { data } = $props();
const { studySpaces, supabase } = $derived(data);
const blankStudySpace = {
description: "",
building_address: "",
location: ""
};
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
...blankStudySpace
});
let fileInput = $state<HTMLInputElement>();
async function uploadStudySpace() {
const imageFile = fileInput?.files?.[0];
if (!imageFile) return alert("Please select an image file.");
const { data: studySpaceInsert, error: studySpaceError } = await supabase
.from("study_spaces")
.insert(studySpaceData)
.select()
.single();
if (studySpaceError)
return alert(`Error uploading study space: ${studySpaceError.message}`);
const { data: imgUpload, error: imageError } = await supabase.storage
.from("files_bucket")
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
contentType: imageFile.type
});
if (imageError) return alert(`Error uploading image: ${imageError.message}`);
const { error: imageInsertError } = await supabase
.from("study_space_images")
.insert({
study_space_id: studySpaceInsert.id,
image_path: imgUpload.path
})
.select()
.single();
if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`);
if (fileInput) fileInput.value = ""; // Clear the file input
studySpaceData = { ...blankStudySpace }; // Reset the form data
alert("Thank you for your contribution!");
invalidate("db:study_spaces"); // Invalidate page data so that it refreshes
}
</script>
<Navbar>
<a href="/space">
<img src={crossUrl} alt="new" class="new-space" />
</a>
</Navbar>
<main>
{#each studySpaces as studySpace (studySpace.id)}
{@const imgUrl =
@@ -62,7 +24,11 @@
.from("files_bucket")
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
: defaultImg}
<SpaceCard alt="Photo of {studySpace.description}" imgSrc={imgUrl}>
<SpaceCard
alt="Photo of {studySpace.description}"
href="/space/{studySpace.id}"
imgSrc={imgUrl}
>
{#snippet description()}
<p>{studySpace.description}</p>
{/snippet}
@@ -70,37 +36,6 @@
{/each}
</main>
<form
onsubmit={(e) => {
e.preventDefault();
uploadStudySpace();
}}
>
<input
type="text"
name="description"
bind:value={studySpaceData.description}
placeholder="Study Space Description"
required
/>
<input
type="text"
name="location"
bind:value={studySpaceData.location}
placeholder="Study Space Location within building"
required
/>
<input
type="text"
name="building_address"
bind:value={studySpaceData.building_address}
placeholder="Building Address"
required
/>
<input type="file" name="image" accept=".png, image/png" required bind:this={fileInput} />
<button type="submit">Upload some Study Space!</button>
</form>
<style>
main {
display: grid;
@@ -109,10 +44,8 @@
padding: 1rem;
width: min(600px, 100vw);
}
form {
max-width: 600px;
display: flex;
flex-direction: column;
gap: 0.5rem;
.new-space {
transform: rotate(45deg);
}
</style>

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import Text from "$lib/components/inputs/Text.svelte";
import Textarea from "$lib/components/inputs/Textarea.svelte";
import Navbar from "$lib/components/Navbar.svelte";
import crossUrl from "$lib/assets/cross.svg";
import Button from "$lib/components/Button.svelte";
import Image from "$lib/components/inputs/Image.svelte";
import type { Table } from "$lib";
import { goto, invalidate } from "$app/navigation";
const { data } = $props();
const { supabase } = $derived(data);
let spaceImg = $state<FileList>();
let studySpaceData = $state<Omit<Table<"study_spaces">, "id" | "created_at" | "updated_at">>({
description: "",
building_address: "",
location: ""
});
async function uploadStudySpace() {
const imageFile = spaceImg?.[0];
if (!imageFile) return alert("Please select an image file.");
const { data: studySpaceInsert, error: studySpaceError } = await supabase
.from("study_spaces")
.insert(studySpaceData)
.select()
.single();
if (studySpaceError)
return alert(`Error uploading study space: ${studySpaceError.message}`);
const { data: imgUpload, error: imageError } = await supabase.storage
.from("files_bucket")
.upload(`public/${studySpaceInsert.id}-${imageFile.name}`, imageFile, {
contentType: imageFile.type
});
if (imageError) return alert(`Error uploading image: ${imageError.message}`);
const { error: imageInsertError } = await supabase
.from("study_space_images")
.insert({
study_space_id: studySpaceInsert.id,
image_path: imgUpload.path
})
.select()
.single();
if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`);
alert("Thank you for your contribution!");
// Redirect to the new study space page
await goto(`/space/${studySpaceInsert.id}`, {
invalidate: ["db:study_spaces"]
});
}
</script>
<Navbar>
<a href="/">
<img src={crossUrl} alt="close" />
</a>
</Navbar>
<form
onsubmit={async (event) => {
event.preventDefault();
await uploadStudySpace();
}}
>
<Image name="study-space-image" minHeight="16rem" bind:files={spaceImg} />
<label for="location">Enter the name:</label>
<Text name="location" bind:value={studySpaceData.location} placeholder="Room 123, Floor 1" />
<label for="description">Add a description:</label>
<Textarea
name="description"
bind:value={studySpaceData.description}
placeholder="A quiet, but small study space..."
rows={5}
/>
<label for="address">Add an address:</label>
<Text
name="address"
bind:value={studySpaceData.building_address}
placeholder="180 Queen's Gate, London, SW7 5HF"
/>
<div class="submit">
<Button type="submit">Share this study space!</Button>
</div>
</form>
<style>
form {
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 0.5rem;
max-width: 32rem;
margin: 0 auto;
}
label {
color: #ffffff;
margin-top: 0.5rem;
}
.submit {
position: sticky;
display: flex;
flex-direction: column;
margin-top: 0.5rem;
bottom: 0;
margin-left: -0.5rem;
width: calc(100% + 1rem);
}
</style>

View File

@@ -0,0 +1,13 @@
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params, locals: { supabase } }) => {
const { data: space, error: err } = await supabase
.from("study_spaces")
.select("*, study_space_images(*)")
.eq("id", params.id)
.single();
if (err) error(500, "Failed to load study space");
return { space };
};

View File

@@ -0,0 +1,97 @@
<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";
const { data } = $props();
const { space, supabase } = $derived(data);
const imgUrl = $derived(
space.study_space_images.length > 0
? supabase.storage
.from("files_bucket")
.getPublicUrl(space.study_space_images[0].image_path).data.publicUrl
: placeholder
);
</script>
<Navbar>
<a href="/">
<img src={crossUrl} alt="close" />
</a>
</Navbar>
<main>
<img src={imgUrl} alt="the study space" />
<div class="nameContainer">
{space.location}
</div>
<p class="descContainer">
{space.description}
</p>
<hr />
<div class="whereSubtitle">Where it is:</div>
<p class="addrContainer">
{space.building_address}
</p>
</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 {
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;
}
.whereSubtitle {
font-size: 1.2rem;
font-weight: bold;
color: #ffffff;
padding: 1.4rem;
}
.addrContainer {
font-size: 1.2rem;
padding: 0rem 1.4rem;
}
</style>