From 61f8bd9de71c5e29664e9aa13835d5651480594e Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 29 Jan 2026 11:40:55 -0600 Subject: [PATCH] refactor: consolidate menu snippets and strengthen type safety Replaces duplicated dropdown/context menu code with parameterized snippet, eliminates unsafe type casts, adds error handling for clipboard and API calls, and improves accessibility annotations. --- web/biome.json | 2 +- web/src/lib/bindings/index.ts | 8 + web/src/lib/components/CourseDetail.svelte | 22 +-- web/src/lib/components/CourseTable.svelte | 168 ++++++++------------ web/src/lib/components/SearchFilters.svelte | 1 + web/src/lib/components/TermCombobox.svelte | 3 + web/src/lib/course.test.ts | 27 +++- web/src/routes/+layout.svelte | 13 +- web/src/routes/+page.svelte | 15 +- web/src/routes/+page.ts | 9 +- 10 files changed, 148 insertions(+), 120 deletions(-) create mode 100644 web/src/lib/bindings/index.ts diff --git a/web/biome.json b/web/biome.json index 6949e12..fc01adc 100644 --- a/web/biome.json +++ b/web/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "ignore": ["dist/", "node_modules/", ".svelte-kit/"] + "ignore": ["dist/", "node_modules/", ".svelte-kit/", "src/lib/bindings/"] }, "formatter": { "enabled": true, diff --git a/web/src/lib/bindings/index.ts b/web/src/lib/bindings/index.ts new file mode 100644 index 0000000..96c06ba --- /dev/null +++ b/web/src/lib/bindings/index.ts @@ -0,0 +1,8 @@ +export type { CodeDescription } from "./CodeDescription"; +export type { CourseResponse } from "./CourseResponse"; +export type { DbMeetingTime } from "./DbMeetingTime"; +export type { InstructorResponse } from "./InstructorResponse"; +export type { SearchResponse } from "./SearchResponse"; +export type { ServiceInfo } from "./ServiceInfo"; +export type { ServiceStatus } from "./ServiceStatus"; +export type { StatusResponse } from "./StatusResponse"; diff --git a/web/src/lib/components/CourseDetail.svelte b/web/src/lib/components/CourseDetail.svelte index 172685f..1749534 100644 --- a/web/src/lib/components/CourseDetail.svelte +++ b/web/src/lib/components/CourseDetail.svelte @@ -18,11 +18,15 @@ let copiedEmail: string | null = $state(null); async function copyEmail(email: string, event: MouseEvent) { event.stopPropagation(); - await navigator.clipboard.writeText(email); - copiedEmail = email; - setTimeout(() => { - copiedEmail = null; - }, 2000); + try { + await navigator.clipboard.writeText(email); + copiedEmail = email; + setTimeout(() => { + copiedEmail = null; + }, 2000); + } catch (err) { + console.error("Failed to copy email:", err); + } } @@ -42,8 +46,8 @@ async function copyEmail(email: string, event: MouseEvent) { class="inline-flex items-center gap-1.5 text-sm font-medium bg-card border border-border rounded-md px-2.5 py-1 text-foreground hover:border-foreground/20 hover:bg-card/80 transition-colors" > {instructor.displayName} - {#if 'rmpRating' in instructor && instructor.rmpRating} - {@const rating = instructor.rmpRating as number} + {#if instructor.rmpRating != null} + {@const rating = instructor.rmpRating} {rating.toFixed(1)}★ @@ -59,9 +63,9 @@ async function copyEmail(email: string, event: MouseEvent) { {#if instructor.isPrimary}
Primary instructor
{/if} - {#if 'rmpRating' in instructor && instructor.rmpRating} + {#if instructor.rmpRating != null}
- {(instructor.rmpRating as number).toFixed(1)}/5 ({(instructor as any).rmpNumRatings ?? 0} ratings) + {instructor.rmpRating.toFixed(1)}/5 ({instructor.rmpNumRatings ?? 0} ratings)
{/if} {#if instructor.email} diff --git a/web/src/lib/components/CourseTable.svelte b/web/src/lib/components/CourseTable.svelte index a5bcd15..e778b37 100644 --- a/web/src/lib/components/CourseTable.svelte +++ b/web/src/lib/components/CourseTable.svelte @@ -262,97 +262,56 @@ const table = createSvelteTable({ }); -{#snippet columnVisibilityItems(variant: "dropdown" | "context")} - {#if variant === "dropdown"} - - + + Toggle columns + + {#each columns as col} + {@const id = col.id!} + {@const label = + typeof col.header === "string" ? col.header : id} + { + columnVisibility = { + ...columnVisibility, + [id]: checked, + }; + }} + class="relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer select-none outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground" > - Toggle columns - - {#each columns as col} - {@const id = col.id!} - {@const label = - typeof col.header === "string" ? col.header : id} - { - columnVisibility = { - ...columnVisibility, - [id]: checked, - }; - }} - class="relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer select-none outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground" - > - {#snippet children({ checked })} - - {#if checked} - - {/if} - - {label} - {/snippet} - - {/each} - - {#if hasCustomVisibility} - - - - Reset to default - - {/if} - {:else} - - - Toggle columns - - {#each columns as col} - {@const id = col.id!} - {@const label = - typeof col.header === "string" ? col.header : id} - { - columnVisibility = { - ...columnVisibility, - [id]: checked, - }; - }} - class="relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer select-none outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground" - > - {#snippet children({ checked })} - - {#if checked} - - {/if} - - {label} - {/snippet} - - {/each} - - {#if hasCustomVisibility} - - - - Reset to default - - {/if} + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {label} + {/snippet} + + {/each} + + {#if hasCustomVisibility} + + + + Reset to default + {/if} {/snippet} @@ -379,7 +338,13 @@ const table = createSvelteTable({ {...props} transition:fly={{ duration: 150, y: -10 }} > - {@render columnVisibilityItems("dropdown")} + {@render columnVisibilityGroup( + DropdownMenu.Group, + DropdownMenu.GroupHeading, + DropdownMenu.CheckboxItem, + DropdownMenu.Separator, + DropdownMenu.Item, + )} {/if} @@ -574,6 +539,7 @@ const table = createSvelteTable({ )} {@const display = primaryInstructorDisplay(course)} {@const commaIdx = display.indexOf(", ")} + {@const ratingData = primaryRating(course)} {#if display === "Staff"} {/if} - {#if primaryRating(course)} - {@const r = - primaryRating(course)!} + {#if ratingData} {r.rating.toFixed( + >{ratingData.rating.toFixed( 1, )}★ @@ -772,7 +736,13 @@ const table = createSvelteTable({ in:fade={{ duration: 100 }} out:fade={{ duration: 100 }} > - {@render columnVisibilityItems("context")} + {@render columnVisibilityGroup( + ContextMenu.Group, + ContextMenu.GroupHeading, + ContextMenu.CheckboxItem, + ContextMenu.Separator, + ContextMenu.Item, + )} {/if} diff --git a/web/src/lib/components/SearchFilters.svelte b/web/src/lib/components/SearchFilters.svelte index 9d15f48..e3facd3 100644 --- a/web/src/lib/components/SearchFilters.svelte +++ b/web/src/lib/components/SearchFilters.svelte @@ -29,6 +29,7 @@ let { +