From 7117f85ef76ec25cec2a986ca387c92520b0ac96 Mon Sep 17 00:00:00 2001 From: Caspar Jojo Asaam Date: Thu, 12 Jun 2025 10:10:41 +0100 Subject: [PATCH 1/3] feat: added migration for a study_space_hours table and allowed for the user to make time inputs when submitting a new space Co-Authored-By: Tadios Temesgen --- src/lib/database.d.ts | 41 ++++++++++ src/routes/space/[id]/edit/+page.server.ts | 13 ++++ src/routes/space/[id]/edit/+page.svelte | 78 ++++++++++++++++++- ...20250612032104_study_space_hours_table.sql | 14 ++++ supabase/schemas/0001_study_spaces.sql | 16 ++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20250612032104_study_space_hours_table.sql diff --git a/src/lib/database.d.ts b/src/lib/database.d.ts index cb19ff3..ca1da1d 100644 --- a/src/lib/database.d.ts +++ b/src/lib/database.d.ts @@ -140,6 +140,47 @@ export type Database = { } Relationships: [] } + study_space_hours: { + Row: { + id: string + study_space_id: string + day_of_week: number + opens_at: string + closes_at: string + is_24_7: boolean + created_at: string | null + updated_at: string | null + } + Insert: { + id?: string + study_space_id: string + day_of_week: number + opens_at: string + closes_at: string + is_24_7: boolean + created_at?: string | null + updated_at?: string | null + } + Update: { + id?: string + study_space_id?: string + day_of_week?: number + opens_at?: string + closes_at?: string + is_24_7?: boolean + created_at?: string | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "study_space_hours_study_space_id_fkey" + columns: ["study_space_id"] + isOneToOne: false + referencedRelation: "study_spaces" + referencedColumns: ["id"] + }, + ] + } } Views: { [_ in never]: never diff --git a/src/routes/space/[id]/edit/+page.server.ts b/src/routes/space/[id]/edit/+page.server.ts index 5ddf15d..78baabf 100644 --- a/src/routes/space/[id]/edit/+page.server.ts +++ b/src/routes/space/[id]/edit/+page.server.ts @@ -8,6 +8,12 @@ type StudySpaceData = Omit< > & { id?: string; building_location?: google.maps.places.PlaceResult; + opening_times?: { + day_of_week: number; + opens_at: string; + closes_at: string; + is_24_7: boolean; + }[]; }; export const load: PageServerLoad = async ({ params, locals: { supabase } }) => { @@ -34,6 +40,13 @@ export const load: PageServerLoad = async ({ params, locals: { supabase } }) => const studySpaceData = space as StudySpaceData & Partial; const images = studySpaceData.study_space_images || []; + const { data: hours, error: hoursErr } = await supabase + .from("study_space_hours") + .select("day_of_week, opens_at, closes_at, is_24_7") + .eq("study_space_id", params.id) + .order("day_of_week", { ascending: true }); + if (hoursErr) error(500, "Failed to load opening times"); + studySpaceData.opening_times = hours; delete studySpaceData.created_at; delete studySpaceData.updated_at; diff --git a/src/routes/space/[id]/edit/+page.svelte b/src/routes/space/[id]/edit/+page.svelte index e04eee2..bcf02ac 100644 --- a/src/routes/space/[id]/edit/+page.svelte +++ b/src/routes/space/[id]/edit/+page.svelte @@ -20,10 +20,35 @@ const { supabase } = $derived(data); const { space, images } = $derived(data); - const studySpaceData = $derived({ + // Days of week for opening times + const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ]; + const studySpaceData = $state({ + opening_times: daysOfWeek.map((_, index) => ({ + day_of_week: index, + opens_at: "", + closes_at: "", + is_24_7: false + })), ...space }); + $effect(() => { + if (!space) return; + const { opening_times, ...rest } = space; + Object.assign(studySpaceData, rest); + if (opening_times) { + studySpaceData.opening_times = opening_times; + } + }); + let scrollPosition = $state(0); const existingImages = $derived( Promise.all( @@ -46,11 +71,13 @@ if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file."); if (!studySpaceData.building_location) return alert("Please select a building location."); + const { opening_times, ...spacePayload } = studySpaceData; + const { data: studySpaceInsert, error: studySpaceError } = await supabase .from("study_spaces") .upsert( { - ...studySpaceData, + ...spacePayload, building_location: studySpaceData.building_location as Json }, { @@ -102,6 +129,23 @@ .select(); if (imageInsertError) return alert(`Error creating image: ${imageInsertError.message}`); + const { error: deleteErr } = await supabase + .from("study_space_hours") + .delete() + .eq("study_space_id", studySpaceInsert.id); + if (deleteErr) return alert(`Error clearing old hours: ${deleteErr.message}`); + + const { error: hoursErr } = await supabase.from("study_space_hours").insert( + opening_times.map((h) => ({ + study_space_id: studySpaceInsert.id, + day_of_week: h.day_of_week, + opens_at: h.opens_at, + closes_at: h.closes_at, + is_24_7: h.is_24_7 + })) + ); + if (hoursErr) return alert(`Error saving opening times: ${hoursErr.message}`); + alert("Thank you for your contribution!"); // Redirect to the new study space page await goto(`/space/${studySpaceInsert.id}`, { @@ -254,6 +298,36 @@ + +
+ {#each daysOfWeek as day, index (index)} +
+ + + to + + +
+ {/each} +
+
{#each studySpaceData.tags as tagName (tagName)} diff --git a/supabase/migrations/20250612032104_study_space_hours_table.sql b/supabase/migrations/20250612032104_study_space_hours_table.sql new file mode 100644 index 0000000..6020722 --- /dev/null +++ b/supabase/migrations/20250612032104_study_space_hours_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE study_space_hours ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + study_space_id UUID REFERENCES study_spaces(id) ON DELETE CASCADE, + day_of_week INT CHECK (day_of_week BETWEEN 0 AND 6), -- 0 = Sunday, 6 = Saturday + opens_at TIME NOT NULL, + closes_at TIME NOT NULL, + is_24_7 BOOLEAN DEFAULT FALSE, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + +CREATE TRIGGER study_space_hours_updated_at +AFTER UPDATE ON study_space_hours +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); \ No newline at end of file diff --git a/supabase/schemas/0001_study_spaces.sql b/supabase/schemas/0001_study_spaces.sql index 134ff96..6ccdccd 100644 --- a/supabase/schemas/0001_study_spaces.sql +++ b/supabase/schemas/0001_study_spaces.sql @@ -18,6 +18,7 @@ CREATE TABLE study_spaces ( volume text NOT NULL, wifi text NOT NULL, power text NOT NULL, + created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now() ); @@ -39,6 +40,17 @@ CREATE TABLE reports ( content text ); +CREATE TABLE study_space_hours ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + study_space_id UUID REFERENCES study_spaces(id) ON DELETE CASCADE, + day_of_week INT CHECK (day_of_week BETWEEN 0 AND 6), -- 0 = Sunday, 6 = Saturday + opens_at TIME NOT NULL, + closes_at TIME NOT NULL, + is_24_7 BOOLEAN DEFAULT FALSE, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + -- Triggers CREATE TRIGGER study_spaces_updated_at AFTER UPDATE ON study_spaces @@ -51,3 +63,7 @@ FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); CREATE TRIGGER reports_updated_at AFTER UPDATE ON reports FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); + +CREATE TRIGGER study_space_hours_updated_at +AFTER UPDATE ON study_space_hours +FOR EACH ROW EXECUTE FUNCTION handle_updated_at(); -- 2.49.1 From afe7b3078d1598381b4a47033042ecb4a742394b Mon Sep 17 00:00:00 2001 From: Caspar Jojo Asaam Date: Thu, 12 Jun 2025 14:59:22 +0100 Subject: [PATCH 2/3] feat: Added current opening times to each study space on the main page. In the expanded card, you can view the opening times for the full week. Improved ui Co-Authored-By: Tadios Temesgen --- src/lib/components/OpeningTimes.svelte | 40 ++++++++++++++++ src/lib/components/SpaceCard.svelte | 5 +- src/lib/index.ts | 18 ++++++- src/routes/+page.server.ts | 2 +- src/routes/+page.svelte | 1 + src/routes/space/[id]/+page.server.ts | 2 +- src/routes/space/[id]/+page.svelte | 45 ++++++++++++++++- src/routes/space/[id]/edit/+page.svelte | 64 ++++++++++++++++++++----- 8 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 src/lib/components/OpeningTimes.svelte diff --git a/src/lib/components/OpeningTimes.svelte b/src/lib/components/OpeningTimes.svelte new file mode 100644 index 0000000..fbd3242 --- /dev/null +++ b/src/lib/components/OpeningTimes.svelte @@ -0,0 +1,40 @@ + + +
+ Today's Opening Times: + {openingDisplay} +
+ + diff --git a/src/lib/components/SpaceCard.svelte b/src/lib/components/SpaceCard.svelte index 0c1bf04..b5c1a19 100644 --- a/src/lib/components/SpaceCard.svelte +++ b/src/lib/components/SpaceCard.svelte @@ -1,15 +1,17 @@ @@ -24,6 +26,7 @@ {/each}
{/if} +
diff --git a/src/lib/index.ts b/src/lib/index.ts index ce66b97..b709d5c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,7 +8,6 @@ export type Enum = Database["public export const availableStudySpaceTags = [ "Crowded", "Group study", - "24/7", "Food allowed", "No food allowed", "Well lit", @@ -43,3 +42,20 @@ export async function gmapsLoader() { libraries: ["places"] }); } + +export function formatTime(time: string) { + const [h, m] = time.split(":").map(Number); + const date = new Date(); + date.setHours(h, m); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +export const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" +]; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index b364832..7f2ccd5 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -5,7 +5,7 @@ export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => depends("db:study_spaces"); const { data: studySpaces, error: err } = await supabase .from("study_spaces") - .select("*, study_space_images(*)"); + .select("*, study_space_images(*), study_space_hours(*)"); if (err) error(500, "Failed to load study spaces"); return { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 094cf78..850aa58 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -137,6 +137,7 @@ href="/space/{studySpace.id}" imgSrc={imgUrl} space={studySpace} + hours={studySpace.study_space_hours} /> {/each} diff --git a/src/routes/space/[id]/+page.server.ts b/src/routes/space/[id]/+page.server.ts index b89b2db..daa172e 100644 --- a/src/routes/space/[id]/+page.server.ts +++ b/src/routes/space/[id]/+page.server.ts @@ -4,7 +4,7 @@ import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ params, locals: { supabase } }) => { const { data: space, error: err } = await supabase .from("study_spaces") - .select("*, study_space_images(*)") + .select("*, study_space_images(*), study_space_hours(*)") .eq("id", params.id) .single(); if (err) error(500, "Failed to load study space"); diff --git a/src/routes/space/[id]/+page.svelte b/src/routes/space/[id]/+page.svelte index a3f151b..91d0699 100644 --- a/src/routes/space/[id]/+page.svelte +++ b/src/routes/space/[id]/+page.svelte @@ -6,7 +6,7 @@ import CompulsoryTags from "$lib/components/CompulsoryTags.svelte"; import Report from "$lib/components/Report.svelte"; import { onMount } from "svelte"; - import { gmapsLoader } from "$lib"; + import { gmapsLoader, daysOfWeek, formatTime } from "$lib"; const { data } = $props(); const { space, supabase } = $derived(data); @@ -45,6 +45,13 @@ map }); }); + + const hoursByDay = $derived(new Map(space.study_space_hours.map((h) => [h.day_of_week, h]))); + + const openingEntries = daysOfWeek.map((day, idx) => ({ + day, + entry: hoursByDay.get(idx) + })); @@ -77,6 +84,22 @@ {/if}
+
Opening Times:
+ {#each openingEntries as { day, entry } (entry)} +
+ {day}: + + {#if entry} + {entry.is_24_7 + ? "Open All Day" + : `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`} + {:else} + Closed + {/if} + +
+ {/each} +
Where it is:

{#if place.name} @@ -220,4 +243,24 @@ text-decoration: none; text-align: center; } + + .opening-entry { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.75rem; + padding: 0.5rem 1.4rem; + align-items: center; + } + .opening-entry .day { + font-weight: bold; + color: #ffffff; + white-space: nowrap; + } + .opening-entry .times { + font-family: monospace; + background-color: rgba(255, 255, 255, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + color: #eaffeb; + } diff --git a/src/routes/space/[id]/edit/+page.svelte b/src/routes/space/[id]/edit/+page.svelte index bcf02ac..5302a2c 100644 --- a/src/routes/space/[id]/edit/+page.svelte +++ b/src/routes/space/[id]/edit/+page.svelte @@ -11,7 +11,8 @@ wifiTags, powerOutletTags, volumeTags, - gmapsLoader + gmapsLoader, + daysOfWeek } from "$lib"; import { onMount } from "svelte"; import type { Json } from "$lib/database.js"; @@ -20,16 +21,6 @@ const { supabase } = $derived(data); const { space, images } = $derived(data); - // Days of week for opening times - const daysOfWeek = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" - ]; const studySpaceData = $state({ opening_times: daysOfWeek.map((_, index) => ({ day_of_week: index, @@ -205,6 +196,20 @@ }); spaceImgs = dt.files; }); + + // --- Helper functions for opening times --- + function toggle247(index: number) { + const ot = studySpaceData.opening_times[index]; + if (ot.is_24_7) { + ot.opens_at = "00:00"; + ot.closes_at = "00:00"; + } + } + + function updateTimes(index: number) { + const ot = studySpaceData.opening_times[index]; + ot.is_24_7 = ot.opens_at === "00:00" && ot.closes_at === "00:00"; + } @@ -308,6 +313,7 @@ type="time" bind:value={studySpaceData.opening_times[index].opens_at} required + onchange={() => updateTimes(index)} /> to updateTimes(index)} /> {/each} @@ -562,4 +570,36 @@ .additionalImages input { display: none; } + + /* Opening times layout and inputs styling */ + .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; + } -- 2.49.1 From 30f44b0ac6b76d2f9bf3b094da87431b5701feb2 Mon Sep 17 00:00:00 2001 From: Caspar Jojo Asaam Date: Thu, 12 Jun 2025 15:40:20 +0100 Subject: [PATCH 3/3] feat: Added current opening times to each study space on the main page. In the expanded card, you can view the opening times for the full week. Improved ui Co-Authored-By: Tadios Temesgen --- src/routes/+page.svelte | 101 ++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 850aa58..1c0f416 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,6 +10,8 @@ let selectedTags = $state([]); let tagFilter = $state(""); + let openingFilter = $state(""); + let closingFilter = $state(""); let tagFilterElem = $state(); function categorySelected(category: string[]) { @@ -32,19 +34,60 @@ }) ); - let filteredStudySpaces = $derived( - selectedTags.length === 0 - ? studySpaces - : studySpaces.filter((space) => { - const allTags = [ - ...(space.tags || []), - space.volume, - space.wifi, - space.power - ].filter(Boolean); + // Convert "HH:MM" or "HH:MM:SS" to minutes since midnight + function toMinutes(timeStr: string): number { + const [h, m] = timeStr.slice(0, 5).split(":").map(Number); + return h * 60 + m; + } - return selectedTags.every((tag) => allTags.includes(tag)); - }) + // Combine tag and time filtering + let filteredStudySpaces = $derived( + studySpaces + // tag filtering + .filter((space) => { + if (selectedTags.length === 0) return true; + const allTags = [ + ...(space.tags || []), + space.volume, + space.wifi, + space.power + ].filter(Boolean); + return selectedTags.every((tag) => allTags.includes(tag)); + }) + // opening time filter + .filter((space) => { + if (!openingFilter) return true; + const entry = space.study_space_hours?.find( + (h) => h.day_of_week === new Date().getDay() + ); + if (!entry) return false; + if (entry.is_24_7) return true; + const openMin = toMinutes(entry.opens_at); + let closeMin = toMinutes(entry.closes_at); + // Treat midnight as end of day and handle overnight spans + if (closeMin === 0) closeMin = 24 * 60; + if (closeMin <= openMin) closeMin += 24 * 60; + const filterMin = toMinutes(openingFilter); + // Include spaces open at the filter time + return filterMin >= openMin && filterMin < closeMin; + }) + // closing time filter + .filter((space) => { + if (!closingFilter) return true; + const entry = space.study_space_hours?.find( + (h) => h.day_of_week === new Date().getDay() + ); + if (!entry) return false; + if (entry.is_24_7) return true; + const openMin = toMinutes(entry.opens_at); + let closeMin = toMinutes(entry.closes_at); + if (closeMin === 0) closeMin = 24 * 60; + if (closeMin <= openMin) closeMin += 24 * 60; + const filterMin = + toMinutes(closingFilter) === 0 ? 24 * 60 : toMinutes(closingFilter); + // Include spaces still open at the filter time + return filterMin > openMin && filterMin <= closeMin; + }) ); let dropdownVisible = $state(false); @@ -73,6 +116,16 @@

Check Reports +
+ + +
@@ -162,7 +215,29 @@ grid-column: 1 / -1; display: flex; justify-content: center; - margin-bottom: 1rem; + margin-bottom: 0.5rem; + } + + .time-filter-container { + grid-column: 1 / -1; + display: flex; + gap: 1rem; + justify-content: center; + margin-bottom: 0.5rem; + } + .time-filter-container label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + color: #eaffeb; + } + .time-filter-container input[type="time"] { + background: none; + border: 2px solid #eaffeb; + border-radius: 0.5rem; + padding: 0.25rem 0.5rem; + color: #eaffeb; } form { -- 2.49.1