From be04f2d8690d47e2b66df06ef2a91866ac1b3697 Mon Sep 17 00:00:00 2001 From: Caspar Jojo Asaam Date: Fri, 13 Jun 2025 03:36:16 +0100 Subject: [PATCH] feat: Implemented Favouriting for a user Co-Authored-By: Tadios Temesgen --- src/lib/components/Favourite.svelte | 40 +++++++++++++ src/lib/components/OpeningTimes.svelte | 2 +- src/lib/components/SpaceCard.svelte | 26 ++++++++- src/lib/database.d.ts | 36 ++++++++++++ src/routes/+page.server.ts | 20 ++++++- src/routes/+page.svelte | 51 ++++++++++++++++- src/routes/space/[id]/+page.svelte | 56 ++++++++++++++++++- src/routes/space/[id]/edit/+page.svelte | 30 ---------- .../migrations/20250612104310_users-admin.sql | 13 +++++ supabase/schemas/0001_users.sql | 13 +++++ 10 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 src/lib/components/Favourite.svelte diff --git a/src/lib/components/Favourite.svelte b/src/lib/components/Favourite.svelte new file mode 100644 index 0000000..1c2ef7f --- /dev/null +++ b/src/lib/components/Favourite.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/src/lib/components/OpeningTimes.svelte b/src/lib/components/OpeningTimes.svelte index 51911bd..177ebd7 100644 --- a/src/lib/components/OpeningTimes.svelte +++ b/src/lib/components/OpeningTimes.svelte @@ -19,7 +19,7 @@ let openingDisplay = $state(""); if (todayHours) { openingDisplay = todayHours.open_today_status - ? "Open 24/7" + ? "Open All Day" : `${formatTime(todayHours.opens_at)} - ${formatTime(todayHours.closes_at)}`; } else { openingDisplay = "Closed"; diff --git a/src/lib/components/SpaceCard.svelte b/src/lib/components/SpaceCard.svelte index b5c1a19..ce07d42 100644 --- a/src/lib/components/SpaceCard.svelte +++ b/src/lib/components/SpaceCard.svelte @@ -1,6 +1,7 @@ - + +
+ +
+ +
+

{space.location}

@@ -84,4 +93,17 @@ gap: 0.3rem; font-size: 0.8rem; } + + .image-container { + position: relative; + } + .image-container .fav-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: rgba(11, 128, 34, 0.4); + border-radius: 50%; + padding: 0.25rem; + z-index: 1; + } diff --git a/src/lib/database.d.ts b/src/lib/database.d.ts index 4976a8e..64842ee 100644 --- a/src/lib/database.d.ts +++ b/src/lib/database.d.ts @@ -202,6 +202,42 @@ export type Database = { } Relationships: [] } + favourite_study_spaces: { + Row: { + user_id: string + study_space_id: string + created_at: string | null + updated_at: string | null + } + Insert: { + user_id: string + study_space_id: string + created_at?: string | null + updated_at?: string | null + } + Update: { + user_id?: string + study_space_id?: string + created_at?: string | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "favourite_study_spaces_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "favourite_study_spaces_study_space_id_fkey" + columns: ["study_space_id"] + isOneToOne: false + referencedRelation: "study_spaces" + referencedColumns: ["id"] + } + ] + } } Views: { [_ in never]: never diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 7f2ccd5..1be79b0 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -8,7 +8,25 @@ export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => .select("*, study_space_images(*), study_space_hours(*)"); if (err) error(500, "Failed to load study spaces"); + const { + data: { session } + } = await supabase.auth.getSession(); + + // Fetch this user’s favourites + let favouriteIds: string[] = []; + if (session?.user?.id) { + const { data: favs, error: favErr } = await supabase + .from("favourite_study_spaces") + .select("study_space_id") + .eq("user_id", session.user.id); + if (!favErr && favs) { + favouriteIds = favs.map((f) => f.study_space_id); + } + } + return { - studySpaces + studySpaces, + session, + favouriteIds }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a9e4052..fa3f5b8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,18 +7,46 @@ import Button from "$lib/components/Button.svelte"; const { data } = $props(); - const { studySpaces, supabase, session, adminMode } = $derived(data); + const { + studySpaces, + supabase, + session, + adminMode, + favouriteIds: initialFavourites = [] + } = $derived(data); let selectedTags = $state([]); let tagFilter = $state(""); + let tagFilterElem = $state(); + let openingFilter = $state(""); let closingFilter = $state(""); - let tagFilterElem = $state(); + + let favouriteIds = $derived(initialFavourites); + let showFavourites = $state(false); function categorySelected(category: string[]) { return category.some((tag) => selectedTags.includes(tag)); } + // Toggle a space in/out of favourites + async function handleToggleFavourite(id: string) { + if (!session?.user) return; + const already = favouriteIds.includes(id); + if (already) { + await supabase + .from("favourite_study_spaces") + .delete() + .match({ user_id: session.user.id, study_space_id: id }); + favouriteIds = favouriteIds.filter((x) => x !== id); + } else { + await supabase + .from("favourite_study_spaces") + .insert([{ user_id: session.user.id, study_space_id: id }]); + favouriteIds = [...favouriteIds, id]; + } + } + let filteredTags = $derived( allTags .filter((tag) => tag.toLowerCase().includes(tagFilter.toLowerCase())) @@ -44,6 +72,8 @@ // Combine tag and time filtering let filteredStudySpaces = $derived( studySpaces + // only include favourites when showFavourites===true + .filter((space) => !showFavourites || favouriteIds?.includes(space.id)) // tag filtering .filter((space) => { if (selectedTags.length === 0) return true; @@ -114,6 +144,9 @@
new + {/if} @@ -198,6 +231,8 @@ imgSrc={imgUrl} space={studySpace} hours={studySpace.study_space_hours} + isFavourite={favouriteIds.includes(studySpace.id)} + onToggleFavourite={() => handleToggleFavourite(studySpace.id)} /> {/each} @@ -356,6 +391,18 @@ font-size: 1.2rem; } + .fav-button { + background: none; + border: none; + color: #eaffeb; + font-size: 1rem; + margin-right: 1rem; + cursor: pointer; + } + .fav-button:hover { + text-decoration: underline; + } + @media (max-width: 20rem) { main { grid-template-columns: 1fr; diff --git a/src/routes/space/[id]/+page.svelte b/src/routes/space/[id]/+page.svelte index 4542bff..9e37048 100644 --- a/src/routes/space/[id]/+page.svelte +++ b/src/routes/space/[id]/+page.svelte @@ -9,6 +9,7 @@ import { onMount } from "svelte"; import { gmapsLoader, daysOfWeek, formatTime, type Table } from "$lib"; import Button from "$lib/components/Button.svelte"; + import Favourite from "$lib/components/Favourite.svelte"; const { data } = $props(); const { space, supabase, adminMode } = $derived(data); @@ -65,6 +66,40 @@ for (const entry of space.study_space_hours) { timingsPerDay[entry.day_of_week].push(entry); } + + let isFavourite = $state(false); + onMount(async () => { + const { + data: { session } + } = await supabase.auth.getSession(); + if (!session?.user) return; + const { data: fav } = await supabase + .from("favourite_study_spaces") + .select("study_space_id") + .match({ user_id: session.user.id, study_space_id: space.id }) + .single(); + isFavourite = !!fav; + }); + + // Toggle a space in/out of favourites + async function handleToggleFavourite() { + const { + data: { session } + } = await supabase.auth.getSession(); + if (!session?.user) return; + if (isFavourite) { + await supabase + .from("favourite_study_spaces") + .delete() + .match({ user_id: session.user.id, study_space_id: space.id }); + isFavourite = false; + } else { + await supabase + .from("favourite_study_spaces") + .insert([{ user_id: session.user.id, study_space_id: space.id }]); + isFavourite = true; + } + } @@ -87,8 +122,11 @@ {/if}
-
- {space.location} +
+
{space.location}
+
+ +
{#if space.description != null && space.description.length > 0}

@@ -300,4 +338,18 @@ font-family: monospace; color: #eaffeb; } + .titleContainer { + display: flex; + align-items: center; + justify-content: space-between; + margin: -0.5rem 0 1rem; + } + .title-fav { + font-size: 2rem; + background: rgba(0, 0, 0, 0.4); + border-radius: 50%; + padding: 0.25rem; + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.7); + color: red; + } diff --git a/src/routes/space/[id]/edit/+page.svelte b/src/routes/space/[id]/edit/+page.svelte index a1d0253..067053d 100644 --- a/src/routes/space/[id]/edit/+page.svelte +++ b/src/routes/space/[id]/edit/+page.svelte @@ -658,34 +658,4 @@ background-color: #eaffeb; border-radius: 5rem; } - .opening-times { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .opening-time-item { - display: flex; - align-items: center; - gap: 0.5rem; - } - - .opening-time-item label { - margin-top: 0; - width: 6rem; - } - - .opening-time-item input[type="time"] { - padding: 0.5rem; - height: 2.5rem; - border-radius: 0.5rem; - border: 2px solid #eaffeb; - background: none; - color: #eaffeb; - } - - .opening-time-item span { - margin: 0 0.5rem; - color: #eaffeb; - } diff --git a/supabase/migrations/20250612104310_users-admin.sql b/supabase/migrations/20250612104310_users-admin.sql index 1db2a4b..b44a698 100644 --- a/supabase/migrations/20250612104310_users-admin.sql +++ b/supabase/migrations/20250612104310_users-admin.sql @@ -26,3 +26,16 @@ $$; CREATE TRIGGER users_handle_new_user AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + +-- Table to store users' favourite study spaces +CREATE TABLE favourite_study_spaces ( + user_id uuid REFERENCES users(id) ON DELETE CASCADE, + study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + PRIMARY KEY (user_id, study_space_id) +); + +CREATE TRIGGER favourite_study_spaces_updated_at +AFTER UPDATE ON favourite_study_spaces +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); diff --git a/supabase/schemas/0001_users.sql b/supabase/schemas/0001_users.sql index e94c8c1..6931379 100644 --- a/supabase/schemas/0001_users.sql +++ b/supabase/schemas/0001_users.sql @@ -26,3 +26,16 @@ $$; CREATE TRIGGER users_handle_new_user AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + +-- Table to store users' favourite study spaces +CREATE TABLE favourite_study_spaces ( + user_id uuid REFERENCES users(id) ON DELETE CASCADE, + study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + PRIMARY KEY (user_id, study_space_id) +); + +CREATE TRIGGER favourite_study_spaces_updated_at +AFTER UPDATE ON favourite_study_spaces +FOR EACH ROW EXECUTE FUNCTION handle_updated_at();