feat: walking skeleton - study space uploads
This commit is contained in:
12
src/routes/+layout.svelte
Normal file
12
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
const { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
18
src/routes/+page.server.ts
Normal file
18
src/routes/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
36
src/routes/api/study_spaces/+server.ts
Normal file
36
src/routes/api/study_spaces/+server.ts
Normal 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 });
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user