feat: walking skeleton - study space uploads

This commit is contained in:
2025-05-30 11:13:35 +01:00
parent 00944faf1e
commit d7db89e13c
12 changed files with 516 additions and 14 deletions

12
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,12 @@
<script lang="ts">
const { children } = $props();
</script>
{@render children?.()}
<style>
:global(body) {
margin: 0;
padding: 0;
}
</style>

View File

@@ -0,0 +1,18 @@
import { createClient } from "@supabase/supabase-js";
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import type { PageServerLoad } from "./$types";
import type { Database } from "$lib/database";
import { error } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ depends }) => {
depends("db:study_spaces");
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const { data: studySpaces, error: err } = await supabase
.from("study_spaces")
.select("*, study_space_images(*)");
if (err) error(500, "Failed to load study spaces");
return {
studySpaces
};
};

View File

@@ -1,2 +1,87 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import SpaceCard from "$lib/components/SpaceCard.svelte";
import defaultImg from "$lib/assets/study_space.png";
import { createClient } from "@supabase/supabase-js";
import type { Database } from "$lib/database";
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import { invalidate } from "$app/navigation";
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const { data } = $props();
const { studySpaces } = $derived(data);
let title = $state("");
let fileInput = $state<HTMLInputElement | null>(null);
async function uploadStudySpace() {
const imageFile = fileInput?.files?.[0];
const imageB64 = imageFile
? await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(imageFile);
})
: null;
if (!imageB64 || !imageFile) {
alert("Please select an image file.");
return;
}
const res = await fetch("/api/study_spaces", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title,
img: imageB64,
imgTitle: imageFile.name
})
});
if (res.ok) {
alert("Study space uploaded successfully!");
await invalidate("db:study_spaces");
} else {
alert("Failed to upload study space.");
}
}
</script>
<main>
{#each studySpaces as studySpace (studySpace.id)}
{@const imgUrl =
studySpace.study_space_images.length > 0
? supabase.storage
.from("files_bucket")
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
: defaultImg}
<SpaceCard alt="Photo of {studySpace.title}" imgSrc={imgUrl}>
{#snippet description()}
<p>{studySpace.title}</p>
{/snippet}
</SpaceCard>
{/each}
</main>
<form
onsubmit={(e) => {
e.preventDefault();
uploadStudySpace();
}}
>
<input type="text" name="title" bind:value={title} placeholder="Study Space Title" required />
<input type="file" name="image" accept=".png, image/png" required bind:this={fileInput} />
<button type="submit">Upload Study Space</button>
</form>
<style>
main {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding: 1rem;
width: min(600px, 100vw);
}
form {
max-width: 600px;
}
</style>

View File

@@ -0,0 +1,36 @@
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import type { Database } from "$lib/database";
import { createClient } from "@supabase/supabase-js";
import { error, type RequestHandler } from "@sveltejs/kit";
export const POST: RequestHandler = async ({ request }) => {
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
const body = await request.json();
const title = body.title;
const imgB64 = body.img;
const imgTitle = body.imgTitle;
if (!title || !imgB64 || !imgTitle) error(400, "Missing required fields: title, img, imgTitle");
const img = await fetch(imgB64).then((res) => res.blob());
const { data: imageData, error: imageError } = await supabase.storage
.from("files_bucket")
.upload(`public/${imgTitle}`, img, {
contentType: img.type,
upsert: false
});
if (imageError) error(500, `Failed to upload image: ${imageError.message}`);
const { data: studySpaceData, error: studySpaceError } = await supabase
.from("study_spaces")
.insert({ title })
.select()
.single();
if (studySpaceError) error(500, "Failed to create study space");
const { error: imageLinkError } = await supabase
.from("study_space_images")
.insert({ study_space_id: studySpaceData.id, image_path: imageData.path });
if (imageLinkError) error(500, "Failed to link image to study space");
return new Response(JSON.stringify({ id: studySpaceData.id }), { status: 200 });
};

View File

@@ -1,11 +0,0 @@
import { describe, test, expect } from "vitest";
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/svelte";
import Page from "./+page.svelte";
describe("/+page.svelte", () => {
test("should render h1", () => {
render(Page);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
});
});