From d7db89e13cec74989afb0288140f3827f06bcd52 Mon Sep 17 00:00:00 2001 From: Gleb Koval Date: Fri, 30 May 2025 11:13:35 +0100 Subject: [PATCH] feat: walking skeleton - study space uploads --- .vscode/extensions.json | 3 +- src/lib/assets/study_space.png | Bin 0 -> 10314 bytes src/lib/components/SpaceCard.svelte | 35 +++ src/lib/database.d.ts | 215 ++++++++++++++++++ src/routes/+layout.svelte | 12 + src/routes/+page.server.ts | 18 ++ src/routes/+page.svelte | 89 +++++++- src/routes/api/study_spaces/+server.ts | 36 +++ src/routes/page.svelte.test.ts | 11 - .../20250530085401_study_spaces_skeleton.sql | 56 +++++ supabase/schemas/0000_common.sql | 10 + supabase/schemas/0001_study_spaces.sql | 45 ++++ 12 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 src/lib/assets/study_space.png create mode 100644 src/lib/components/SpaceCard.svelte create mode 100644 src/lib/database.d.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/api/study_spaces/+server.ts delete mode 100644 src/routes/page.svelte.test.ts create mode 100644 supabase/migrations/20250530085401_study_spaces_skeleton.sql create mode 100644 supabase/schemas/0000_common.sql create mode 100644 supabase/schemas/0001_study_spaces.sql diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 38cd1c0..9b8df49 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "svelte.svelte-vscode", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "naumovs.color-highlight" ] } diff --git a/src/lib/assets/study_space.png b/src/lib/assets/study_space.png new file mode 100644 index 0000000000000000000000000000000000000000..9f8d3f4f110242dd8bf0190a973d3b091e62dc7c GIT binary patch literal 10314 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

lZbe#C}Atj{Dy^i0;_0t&f|B9jp9WX!=C% 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 + import type { Snippet } from "svelte"; + + interface Props { + alt: string; + imgSrc: string; + description?: Snippet; + } + + const { alt, imgSrc, description }: Props = $props(); + + +

+ +
+ {@render description?.()} +
+
+ + diff --git a/src/lib/database.d.ts b/src/lib/database.d.ts new file mode 100644 index 0000000..c1b0917 --- /dev/null +++ b/src/lib/database.d.ts @@ -0,0 +1,215 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + study_space_images: { + Row: { + created_at: string | null + image_path: string + study_space_id: string + updated_at: string | null + } + Insert: { + created_at?: string | null + image_path: string + study_space_id: string + updated_at?: string | null + } + Update: { + created_at?: string | null + image_path?: string + study_space_id?: string + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "study_space_images_study_space_id_fkey" + columns: ["study_space_id"] + isOneToOne: false + referencedRelation: "study_spaces" + referencedColumns: ["id"] + }, + ] + } + study_spaces: { + Row: { + created_at: string | null + id: string + title: string + updated_at: string | null + } + Insert: { + created_at?: string | null + id?: string + title: string + updated_at?: string | null + } + Update: { + created_at?: string | null + id?: string + title?: string + updated_at?: string | null + } + Relationships: [] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type DefaultSchema = Database[Extract] + +export type Tables< + DefaultSchemaTableNameOrOptions extends + | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & + Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & + DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & + DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + DefaultSchemaTableNameOrOptions extends + | keyof DefaultSchema["Tables"] + | { schema: keyof Database }, + TableName extends DefaultSchemaTableNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] + ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + DefaultSchemaEnumNameOrOptions extends + | keyof DefaultSchema["Enums"] + | { schema: keyof Database }, + EnumName extends DefaultSchemaEnumNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } + ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] + : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] + ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof DefaultSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] + ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + +export const Constants = { + graphql_public: { + Enums: {}, + }, + public: { + Enums: {}, + }, +} as const diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..9c2d011 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,12 @@ + + +{@render children?.()} + + diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..10d8757 --- /dev/null +++ b/src/routes/+page.server.ts @@ -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(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 + }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..c0591a0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,87 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + +
+ {#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} + + {#snippet description()} +

{studySpace.title}

+ {/snippet} +
+ {/each} +
+ +
{ + e.preventDefault(); + uploadStudySpace(); + }} +> + + + +
+ + diff --git a/src/routes/api/study_spaces/+server.ts b/src/routes/api/study_spaces/+server.ts new file mode 100644 index 0000000..da4b54c --- /dev/null +++ b/src/routes/api/study_spaces/+server.ts @@ -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(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 }); +}; diff --git a/src/routes/page.svelte.test.ts b/src/routes/page.svelte.test.ts deleted file mode 100644 index e4d5d75..0000000 --- a/src/routes/page.svelte.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/supabase/migrations/20250530085401_study_spaces_skeleton.sql b/supabase/migrations/20250530085401_study_spaces_skeleton.sql new file mode 100644 index 0000000..b2ebf50 --- /dev/null +++ b/supabase/migrations/20250530085401_study_spaces_skeleton.sql @@ -0,0 +1,56 @@ +CREATE FUNCTION handle_updated_at() +RETURNS trigger +LANGUAGE plpgsql + SET search_path = '' +AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$; + +INSERT INTO storage.buckets (id, name, public) +VALUES ('files_bucket', 'files_bucket', true); + +CREATE POLICY "Whack" + ON storage.objects + FOR ALL + USING (bucket_id = 'files_bucket'); + +CREATE TABLE study_spaces ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + title text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + +CREATE TABLE study_space_images ( + study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE, + image_path text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + PRIMARY KEY (study_space_id, image_path) +); + +-- Triggers +CREATE TRIGGER study_spaces_updated_at +AFTER UPDATE ON study_spaces +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER study_space_images_updated_at +AFTER UPDATE ON study_space_images +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +-- Security +-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY; + +-- CREATE POLICY "Allow all users to view study spaces" +-- ON study_spaces +-- FOR SELECT +-- USING (true); + +-- CREATE POLICY "Allow all users to view study space images" +-- ON study_space_images +-- FOR SELECT +-- USING (true); \ No newline at end of file diff --git a/supabase/schemas/0000_common.sql b/supabase/schemas/0000_common.sql new file mode 100644 index 0000000..583f64f --- /dev/null +++ b/supabase/schemas/0000_common.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION handle_updated_at() +RETURNS trigger +LANGUAGE plpgsql + SET search_path = '' +AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$; diff --git a/supabase/schemas/0001_study_spaces.sql b/supabase/schemas/0001_study_spaces.sql new file mode 100644 index 0000000..b20d189 --- /dev/null +++ b/supabase/schemas/0001_study_spaces.sql @@ -0,0 +1,45 @@ +INSERT INTO storage.buckets (id, name, public) +VALUES ('files_bucket', 'files_bucket', true); + +CREATE POLICY "Whack" + ON storage.objects + FOR ALL + USING (bucket_id = 'files_bucket'); + +CREATE TABLE study_spaces ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + title text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + +CREATE TABLE study_space_images ( + study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE, + image_path text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + PRIMARY KEY (study_space_id, image_path) +); + +-- Triggers +CREATE TRIGGER study_spaces_updated_at +AFTER UPDATE ON study_spaces +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER study_space_images_updated_at +AFTER UPDATE ON study_space_images +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +-- Security +-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY; + +-- CREATE POLICY "Allow all users to view study spaces" +-- ON study_spaces +-- FOR SELECT +-- USING (true); + +-- CREATE POLICY "Allow all users to view study space images" +-- ON study_space_images +-- FOR SELECT +-- USING (true); \ No newline at end of file