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 0000000..9f8d3f4
Binary files /dev/null and b/src/lib/assets/study_space.png differ
diff --git a/src/lib/components/SpaceCard.svelte b/src/lib/components/SpaceCard.svelte
new file mode 100644
index 0000000..f599a4a
--- /dev/null
+++ b/src/lib/components/SpaceCard.svelte
@@ -0,0 +1,35 @@
+
+
+
+

+
+ {@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}
+
+
+
+
+
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