From 8be06bba8bc1a79b858d25dd277a3493543cb79d Mon Sep 17 00:00:00 2001 From: Barf-Vader <47476490+Barf-Vader@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:57:23 +0100 Subject: [PATCH 1/2] refactor: simpler timing inputs --- src/lib/components/Button.svelte | 5 +- src/lib/components/CompulsoryTags.svelte | 2 +- src/lib/components/OpeningTimes.svelte | 2 +- .../components/inputs/OpeningTimesDay.svelte | 132 +++++++++++ src/lib/database.d.ts | 83 +++---- src/lib/index.ts | 9 +- src/routes/+page.svelte | 4 +- src/routes/space/[id]/+page.svelte | 69 ++++-- src/routes/space/[id]/edit/+page.server.ts | 4 +- src/routes/space/[id]/edit/+page.svelte | 216 ++++++++++++------ .../migrations/20250612170906_renamed-247.sql | 5 + .../20250612180923_not-null-dayofweek.sql | 3 + supabase/schemas/0001_study_spaces.sql | 4 +- supabase/schemas/0001_users.sql | 2 +- 14 files changed, 400 insertions(+), 140 deletions(-) create mode 100644 src/lib/components/inputs/OpeningTimesDay.svelte create mode 100644 supabase/migrations/20250612170906_renamed-247.sql create mode 100644 supabase/migrations/20250612180923_not-null-dayofweek.sql diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 8944bc2..a1e724f 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -5,7 +5,7 @@ onclick?: (event: MouseEvent) => void; disabled?: boolean; type?: "button" | "submit" | "reset"; - style?: "normal" | "red"; + style?: "normal" | "red" | "invisible"; formaction?: string; children?: Snippet; } @@ -45,6 +45,9 @@ .red { background-color: #bd4949; } + .invisible { + background: none; + } .button:focus { outline: 2px solid #007bff; } diff --git a/src/lib/components/CompulsoryTags.svelte b/src/lib/components/CompulsoryTags.svelte index ba5a23b..a8d205e 100644 --- a/src/lib/components/CompulsoryTags.svelte +++ b/src/lib/components/CompulsoryTags.svelte @@ -12,7 +12,7 @@ "No Outlets": "compulsoryTagRed", "Some Outlets": "compulsoryTagYellow", "Good WiFi": "compulsoryTagGreen", - "Bad WiFi": "compulsoryTagRed", + "Bad/No WiFi": "compulsoryTagRed", "Moderate WiFi": "compulsoryTagYellow", "No WiFi": "compulsoryTagRed" }; diff --git a/src/lib/components/OpeningTimes.svelte b/src/lib/components/OpeningTimes.svelte index fbd3242..51911bd 100644 --- a/src/lib/components/OpeningTimes.svelte +++ b/src/lib/components/OpeningTimes.svelte @@ -18,7 +18,7 @@ // Compute the display string for opening times let openingDisplay = $state(""); if (todayHours) { - openingDisplay = todayHours.is_24_7 + openingDisplay = todayHours.open_today_status ? "Open 24/7" : `${formatTime(todayHours.opens_at)} - ${formatTime(todayHours.closes_at)}`; } else { diff --git a/src/lib/components/inputs/OpeningTimesDay.svelte b/src/lib/components/inputs/OpeningTimesDay.svelte new file mode 100644 index 0000000..486e9c4 --- /dev/null +++ b/src/lib/components/inputs/OpeningTimesDay.svelte @@ -0,0 +1,132 @@ + + +
+ {#if day <= 6} + + {:else} + + {/if} + + + {#if onHide} + + {/if} + {#if openTodayStatus === null} +
+ + to + +
+ {/if} +
+ + diff --git a/src/lib/database.d.ts b/src/lib/database.d.ts index 6092f07..4976a8e 100644 --- a/src/lib/database.d.ts +++ b/src/lib/database.d.ts @@ -69,6 +69,47 @@ export type Database = { }, ] } + study_space_hours: { + Row: { + closes_at: string + created_at: string | null + day_of_week: number + id: string + open_today_status: boolean | null + opens_at: string + study_space_id: string | null + updated_at: string | null + } + Insert: { + closes_at: string + created_at?: string | null + day_of_week: number + id?: string + open_today_status?: boolean | null + opens_at: string + study_space_id?: string | null + updated_at?: string | null + } + Update: { + closes_at?: string + created_at?: string | null + day_of_week?: number + id?: string + open_today_status?: boolean | null + opens_at?: string + study_space_id?: 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"] + }, + ] + } study_space_images: { Row: { created_at: string | null @@ -161,47 +202,6 @@ 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 @@ -331,3 +331,4 @@ export const Constants = { Enums: {}, }, } as const + diff --git a/src/lib/index.ts b/src/lib/index.ts index b709d5c..290e2b5 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -57,5 +57,12 @@ export const daysOfWeek = [ "Wednesday", "Thursday", "Friday", - "Saturday" + "Saturday", + "All Days", + "All Other Days" ]; + +export function timeToMins(time: string) { + const [hour, min] = time.split(":"); + return Number(hour) * 60 + Number(min); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2c9b3a3..a9e4052 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -62,7 +62,7 @@ (h) => h.day_of_week === new Date().getDay() ); if (!entry) return false; - if (entry.is_24_7) return true; + if (entry.open_today_status) return true; const openMin = toMinutes(entry.opens_at); let closeMin = toMinutes(entry.closes_at); // Treat midnight as end of day and handle overnight spans @@ -79,7 +79,7 @@ (h) => h.day_of_week === new Date().getDay() ); if (!entry) return false; - if (entry.is_24_7) return true; + if (entry.open_today_status) return true; const openMin = toMinutes(entry.opens_at); let closeMin = toMinutes(entry.closes_at); if (closeMin === 0) closeMin = 24 * 60; diff --git a/src/routes/space/[id]/+page.svelte b/src/routes/space/[id]/+page.svelte index d23f01c..5d0fb6d 100644 --- a/src/routes/space/[id]/+page.svelte +++ b/src/routes/space/[id]/+page.svelte @@ -7,8 +7,9 @@ import Report from "$lib/components/Report.svelte"; import Feedback from "$lib/components/Feedback.svelte"; import { onMount } from "svelte"; - import { gmapsLoader, daysOfWeek, formatTime } from "$lib"; + import { gmapsLoader, daysOfWeek, formatTime, type Table } from "$lib"; import Button from "$lib/components/Button.svelte"; + import TagFilter from "$lib/components/inputs/TagFilter.svelte"; const { data } = $props(); const { space, supabase, adminMode } = $derived(data); @@ -51,12 +52,20 @@ }); }); - const hoursByDay = $derived(new Map(space.study_space_hours.map((h) => [h.day_of_week, h]))); + // Collect all timing entries + let timingsPerDay: Record[]> = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [] + }; - const openingEntries = daysOfWeek.map((day, idx) => ({ - day, - entry: hoursByDay.get(idx) - })); + for (const entry of space.study_space_hours) { + timingsPerDay[entry.day_of_week].push(entry); + } @@ -100,20 +109,26 @@ {/if}
Opening Times:
- {#each openingEntries as { day, entry } (entry)} + {#each Array(7) as _, idx (idx)} + {@const entries = timingsPerDay[idx]}
- {day}: - - {#if entry} - {entry.is_24_7 - ? "Open All Day" - : `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`} + {daysOfWeek[idx]} +
+ {#each entries as entry (entry)} + + {entry.open_today_status + ? "Open All Day" + : entry.open_today_status === false + ? "Closed" + : `${formatTime(entry.opens_at)} – ${formatTime(entry.closes_at)}`} + {:else} - Closed - {/if} - + Not known + {/each} +
{/each} +
Where it is:

@@ -167,7 +182,7 @@ background-color: #2e3c42; width: 70%; border: none; - margin: 0 auto; + margin: 1rem auto 0; } .nameContainer { @@ -260,22 +275,30 @@ } .opening-entry { - display: grid; - grid-template-columns: auto 1fr; + display: flex; gap: 0.75rem; padding: 0.5rem 1.4rem; align-items: center; + background-color: #2e4653; + margin: 0.2rem; + border-radius: 0.5rem; } .opening-entry .day { font-weight: bold; color: #ffffff; - white-space: nowrap; + align-items: center; + justify-content: center; } .opening-entry .times { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 0.25rem 1.5rem; + flex: 1; + align-items: end; + } + .opening-entry .time { 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.server.ts b/src/routes/space/[id]/edit/+page.server.ts index 78baabf..66d2c65 100644 --- a/src/routes/space/[id]/edit/+page.server.ts +++ b/src/routes/space/[id]/edit/+page.server.ts @@ -12,7 +12,7 @@ type StudySpaceData = Omit< day_of_week: number; opens_at: string; closes_at: string; - is_24_7: boolean; + open_today_status: boolean | null; }[]; }; @@ -42,7 +42,7 @@ export const load: PageServerLoad = async ({ params, locals: { supabase } }) => 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") + .select("day_of_week, opens_at, closes_at, open_today_status") .eq("study_space_id", params.id) .order("day_of_week", { ascending: true }); if (hoursErr) error(500, "Failed to load opening times"); diff --git a/src/routes/space/[id]/edit/+page.svelte b/src/routes/space/[id]/edit/+page.svelte index 5302a2c..505f3e1 100644 --- a/src/routes/space/[id]/edit/+page.svelte +++ b/src/routes/space/[id]/edit/+page.svelte @@ -6,13 +6,15 @@ import crossUrl from "$lib/assets/cross.svg"; import Button from "$lib/components/Button.svelte"; import Images from "$lib/components/inputs/Images.svelte"; + import OpeningTimesDay from "$lib/components/inputs/OpeningTimesDay.svelte"; import { availableStudySpaceTags, wifiTags, powerOutletTags, volumeTags, gmapsLoader, - daysOfWeek + daysOfWeek, + timeToMins } from "$lib"; import { onMount } from "svelte"; import type { Json } from "$lib/database.js"; @@ -21,13 +23,15 @@ const { supabase } = $derived(data); const { space, images } = $derived(data); + + interface OpeningTime { + day_of_week: number; + opens_at: string; + closes_at: string; + open_today_status: boolean | null; + } const studySpaceData = $state({ - opening_times: daysOfWeek.map((_, index) => ({ - day_of_week: index, - opens_at: "", - closes_at: "", - is_24_7: false - })), + opening_times: [] as OpeningTime[], ...space }); @@ -57,8 +61,79 @@ ); let spaceImgs = $state(); let uploading = $state(false); + function checkTimings() { + console.log(studySpaceData.opening_times); + let cannotExist = [] as number[]; + + const opensAtMinsAll = timeToMins(allDays.opens_at); + const closesAtMinsAll = timeToMins(allDays.closes_at); + if (opensAtMinsAll >= closesAtMinsAll) { + alert(`Opening time for all days is after closing time.`); + return false; + } + + for (const entry of studySpaceData.opening_times) { + if (cannotExist.includes(entry.day_of_week)) { + alert( + "You marked a day as either closed or open all day, and then provided another timing." + ); + return false; + } + if (entry.open_today_status != null) { + cannotExist.push(entry.day_of_week); + } + const opensAtMins = timeToMins(entry.opens_at); + const closesAtMins = timeToMins(entry.closes_at); + if (opensAtMins >= closesAtMins) { + alert(`Opening time for ${daysOfWeek[entry.day_of_week]} is after closing time.`); + return false; + } + } + return true; + } + + function genTimings(studySpaceId: string) { + const fullDayOfWeek = [0, 1, 2, 3, 4, 5, 6]; + console.log(studySpaceData.opening_times, allDays); + // all day only + if ( + studySpaceData.opening_times.length === 0 && + ((allDays.closes_at != "" && allDays.opens_at != "") || + allDays.open_today_status != null) + ) { + return fullDayOfWeek.map((day) => ({ + study_space_id: studySpaceId, + day_of_week: day, + opens_at: allDays.open_today_status == null ? allDays.opens_at : "00:00", + closes_at: allDays.open_today_status == null ? allDays.closes_at : "00:00", + open_today_status: allDays.open_today_status + })); + } + // some days specified + const nonDefinedDays = fullDayOfWeek.filter( + (day) => !new Set(studySpaceData.opening_times.map((h) => h.day_of_week)).has(day) + ); + return studySpaceData.opening_times + .map((h) => ({ + study_space_id: studySpaceId, + day_of_week: h.day_of_week, + opens_at: h.opens_at, + closes_at: h.closes_at, + open_today_status: h.open_today_status + })) + .concat( + nonDefinedDays.map((day) => ({ + study_space_id: studySpaceId, + day_of_week: day, + opens_at: allDays.opens_at, + closes_at: allDays.closes_at, + open_today_status: allDays.open_today_status + })) + ); + } async function uploadStudySpace() { + if (!checkTimings()) return; if (!spaceImgs || spaceImgs.length < 1) return alert("Please select an image file."); if (!studySpaceData.building_location) return alert("Please select a building location."); @@ -126,17 +201,16 @@ .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}`); - + // Nothing is provided + if ( + (allDays.closes_at != "" && allDays.opens_at != "") || + allDays.open_today_status != null + ) { + const { error: hoursErr } = await supabase + .from("study_space_hours") + .insert(genTimings(studySpaceInsert.id)); + 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}`, { @@ -196,20 +270,12 @@ }); 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"; - } + // Opening times + let allDays = $state({ + opens_at: "", + closes_at: "", + open_today_status: null + }); @@ -302,41 +368,43 @@ - - -

- {#each daysOfWeek as day, index (index)} -
- - updateTimes(index)} - /> - to - updateTimes(index)} - /> - -
+ +
+ {#each studySpaceData.opening_times as opening_time, index (index)} + { + studySpaceData.opening_times.splice(index, 1); + }} + /> +
{/each} +
+ - +
{#each studySpaceData.tags as tagName (tagName)}
- +