diff --git a/web/bun.lock b/web/bun.lock
index 3e7d6ae..8d9232e 100644
--- a/web/bun.lock
+++ b/web/bun.lock
@@ -33,6 +33,7 @@
"jsdom": "^26.1.0",
"svelte": "^5.49.1",
"svelte-check": "^4.3.5",
+ "svelte-range-slider-pips": "^4.1.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
@@ -640,6 +641,8 @@
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
+ "svelte-range-slider-pips": ["svelte-range-slider-pips@4.1.0", "", { "peerDependencies": { "svelte": "^4.2.7 || ^5.0.0" } }, "sha512-2Zw7MngIuPeqdyJ3ueEp7jPSx0hce+Sx8r1eteCeUPxEWlNavKhBtqJyuoAdpvh5csPPFVZJ4TJ4MX9s4G70uw=="],
+
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
diff --git a/web/package.json b/web/package.json
index 316c5c7..4f9326e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -31,6 +31,7 @@
"jsdom": "^26.1.0",
"svelte": "^5.49.1",
"svelte-check": "^4.3.5",
+ "svelte-range-slider-pips": "^4.1.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
diff --git a/web/src/lib/components/AttributesPopover.svelte b/web/src/lib/components/AttributesPopover.svelte
index ea2a27d..eae6368 100644
--- a/web/src/lib/components/AttributesPopover.svelte
+++ b/web/src/lib/components/AttributesPopover.svelte
@@ -1,8 +1,6 @@
-
-
- {#if hasActiveFilters}
-
- {/if}
- Attributes
-
-
-
- {#snippet child({ wrapperProps, props, open })}
- {#if open}
-
-
-
- {#each sections as { label, key, dataKey }, i (key)}
- {#if i > 0}
-
- {/if}
-
-
{label}
-
- {#each referenceData[dataKey] as item (item.code)}
- {@const selected = getSelected(key)}
-
- {/each}
-
-
- {/each}
-
-
-
+
+ {#snippet content()}
+ {#each sections as { label, key, dataKey }, i (key)}
+ {#if i > 0}
+
{/if}
- {/snippet}
-
-
+
+
{label}
+
+ {#each referenceData[dataKey] as item (item.code)}
+ {@const selected = getSelected(key)}
+
+ {/each}
+
+
+ {/each}
+ {/snippet}
+
diff --git a/web/src/lib/components/FilterPopover.svelte b/web/src/lib/components/FilterPopover.svelte
new file mode 100644
index 0000000..e91cf87
--- /dev/null
+++ b/web/src/lib/components/FilterPopover.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+ {#if active}
+
+ {/if}
+ {label}
+
+
+
+ {#snippet child({ wrapperProps, props, open })}
+ {#if open}
+
+
+
+ {@render content()}
+
+
+
+ {/if}
+ {/snippet}
+
+
diff --git a/web/src/lib/components/MorePopover.svelte b/web/src/lib/components/MorePopover.svelte
index 6a96276..ff667d1 100644
--- a/web/src/lib/components/MorePopover.svelte
+++ b/web/src/lib/components/MorePopover.svelte
@@ -1,22 +1,20 @@
-
-
- {#if hasActiveFilters}
-
- {/if}
- More
-
-
-
- {#snippet child({ wrapperProps, props, open })}
- {#if open}
-
-
-
-
+
+ {#snippet content()}
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
- {/if}
- {/snippet}
-
-
+
+ {/snippet}
+
diff --git a/web/src/lib/components/RangeSlider.svelte b/web/src/lib/components/RangeSlider.svelte
index f2c84e0..5939151 100644
--- a/web/src/lib/components/RangeSlider.svelte
+++ b/web/src/lib/components/RangeSlider.svelte
@@ -1,110 +1,223 @@
-
+
+
diff --git a/web/src/lib/components/SchedulePopover.svelte b/web/src/lib/components/SchedulePopover.svelte
index 3464041..5ffe199 100644
--- a/web/src/lib/components/SchedulePopover.svelte
+++ b/web/src/lib/components/SchedulePopover.svelte
@@ -1,7 +1,5 @@
-
-
- {#if hasActiveFilters}
-
- {/if}
- Schedule
-
-
-
- {#snippet child({ wrapperProps, props, open })}
- {#if open}
-
-
-
-
-
Days of week
-
- {#each DAY_OPTIONS as { label, value } (value)}
-
- {/each}
-
-
+
+ {#snippet content()}
+
+
Days of week
+
+ {#each DAY_OPTIONS as { label, value } (value)}
+
+ {/each}
+
+
-
+
-
-
-
-
- {/if}
- {/snippet}
-
-
+
+ {/snippet}
+
diff --git a/web/src/lib/components/SearchFilters.svelte b/web/src/lib/components/SearchFilters.svelte
index 7c2ca33..d4bef0c 100644
--- a/web/src/lib/components/SearchFilters.svelte
+++ b/web/src/lib/components/SearchFilters.svelte
@@ -25,8 +25,8 @@ let {
creditHourMin = $bindable(),
creditHourMax = $bindable(),
instructor = $bindable(),
- courseNumberLow = $bindable(),
- courseNumberHigh = $bindable(),
+ courseNumberMin = $bindable(),
+ courseNumberMax = $bindable(),
referenceData,
ranges,
}: {
@@ -47,8 +47,8 @@ let {
creditHourMin: number | null;
creditHourMax: number | null;
instructor: string;
- courseNumberLow: number | null;
- courseNumberHigh: number | null;
+ courseNumberMin: number | null;
+ courseNumberMax: number | null;
referenceData: {
instructionalMethods: CodeDescription[];
campuses: CodeDescription[];
@@ -95,8 +95,8 @@ let {
bind:creditHourMin
bind:creditHourMax
bind:instructor
- bind:courseNumberLow
- bind:courseNumberHigh
+ bind:courseNumberMin
+ bind:courseNumberMax
ranges={{ courseNumber: ranges.courseNumber, creditHours: ranges.creditHours }}
/>
diff --git a/web/src/lib/components/StatusPopover.svelte b/web/src/lib/components/StatusPopover.svelte
index 7657e74..db8084d 100644
--- a/web/src/lib/components/StatusPopover.svelte
+++ b/web/src/lib/components/StatusPopover.svelte
@@ -1,7 +1,5 @@
-
-
- {#if hasActiveFilters}
-
+
+ {#snippet content()}
+
+ Availability
+
+
+
+
+
+ {#if waitCountMaxRange > 0}
+ (v === 0 ? "Off" : String(v))}
+ />
+ {:else}
+
+ Max waitlist
+ No waitlisted courses
+
{/if}
- Status
-
-
-
- {#snippet child({ wrapperProps, props, open })}
- {#if open}
-
-
-
-
- Availability
-
-
-
-
-
- {#if waitCountMaxRange > 0}
-
v === 0 ? "Off" : String(v)}
- />
- {:else}
-
- Max waitlist
- No waitlisted courses
-
- {/if}
-
-
-
- {/if}
- {/snippet}
-
-
+ {/snippet}
+
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index 08e863e..1e63de4 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -64,8 +64,8 @@ let attributes: string[] = $state(initialParams.getAll("attributes"));
let creditHourMin = $state(parseNumParam("credit_hour_min"));
let creditHourMax = $state(parseNumParam("credit_hour_max"));
let instructor = $state(initialParams.get("instructor") ?? "");
-let courseNumberLow = $state(parseNumParam("course_number_low"));
-let courseNumberHigh = $state(parseNumParam("course_number_high"));
+let courseNumberMin = $state(parseNumParam("course_number_low"));
+let courseNumberMax = $state(parseNumParam("course_number_high"));
let searchOptions = $state(null);
@@ -171,8 +171,8 @@ const THROTTLE_MS = {
creditHourMin: 300,
creditHourMax: 300,
instructor: 300,
- courseNumberLow: 300,
- courseNumberHigh: 300,
+ courseNumberMin: 300,
+ courseNumberMax: 300,
} as const;
let searchTimeout: ReturnType | undefined;
@@ -198,8 +198,8 @@ function buildSearchKey(): string {
creditHourMin,
creditHourMax,
instructor,
- courseNumberLow,
- courseNumberHigh,
+ courseNumberMin,
+ courseNumberMax,
].join("|");
}
@@ -318,14 +318,14 @@ $effect(() => {
});
$effect(() => {
- courseNumberLow;
- scheduleSearch("courseNumberLow");
+ courseNumberMin;
+ scheduleSearch("courseNumberMin");
return () => clearTimeout(searchTimeout);
});
$effect(() => {
- courseNumberHigh;
- scheduleSearch("courseNumberHigh");
+ courseNumberMax;
+ scheduleSearch("courseNumberMax");
return () => clearTimeout(searchTimeout);
});
@@ -347,8 +347,8 @@ function buildFilterKey(): string {
creditHourMin,
creditHourMax,
instructor,
- courseNumberLow,
- courseNumberHigh,
+ courseNumberMin,
+ courseNumberMax,
].join("|");
}
@@ -394,8 +394,8 @@ async function performSearch() {
if (creditHourMin !== null) params.set("credit_hour_min", String(creditHourMin));
if (creditHourMax !== null) params.set("credit_hour_max", String(creditHourMax));
if (instructor) params.set("instructor", instructor);
- if (courseNumberLow !== null) params.set("course_number_low", String(courseNumberLow));
- if (courseNumberHigh !== null) params.set("course_number_high", String(courseNumberHigh));
+ if (courseNumberMin !== null) params.set("course_number_low", String(courseNumberMin));
+ if (courseNumberMax !== null) params.set("course_number_high", String(courseNumberMax));
// Include term in URL only when it differs from the default or other params are active
const hasOtherParams = params.size > 0;
@@ -439,8 +439,8 @@ async function performSearch() {
creditHourMin: creditHourMin ?? undefined,
creditHourMax: creditHourMax ?? undefined,
instructor: instructor || undefined,
- courseNumberLow: courseNumberLow ?? undefined,
- courseNumberHigh: courseNumberHigh ?? undefined,
+ courseNumberLow: courseNumberMin ?? undefined,
+ courseNumberHigh: courseNumberMax ?? undefined,
});
const applyUpdate = () => {
@@ -548,7 +548,7 @@ let activeFilterCount = $derived(
(attributes.length > 0 ? 1 : 0) +
(creditHourMin !== null || creditHourMax !== null ? 1 : 0) +
(instructor !== "" ? 1 : 0) +
- (courseNumberLow !== null || courseNumberHigh !== null ? 1 : 0)
+ (courseNumberMin !== null || courseNumberMax !== null ? 1 : 0)
);
function removeSubject(code: string) {
@@ -569,8 +569,8 @@ function clearAllFilters() {
creditHourMin = null;
creditHourMax = null;
instructor = "";
- courseNumberLow = null;
- courseNumberHigh = null;
+ courseNumberMin = null;
+ courseNumberMax = null;
}
@@ -669,17 +669,17 @@ function clearAllFilters() {
onRemove={() => (instructor = "")}
/>
{/if}
- {#if courseNumberLow !== null || courseNumberHigh !== null}
+ {#if courseNumberMin !== null || courseNumberMax !== null}
{
- courseNumberLow = null;
- courseNumberHigh = null;
+ courseNumberMin = null;
+ courseNumberMax = null;
}}
/>
{/if}
@@ -801,8 +801,8 @@ function clearAllFilters() {
bind:creditHourMin
bind:creditHourMax
bind:instructor
- bind:courseNumberLow
- bind:courseNumberHigh
+ bind:courseNumberMin
+ bind:courseNumberMax
{referenceData}
ranges={{
courseNumber: { min: ranges.courseNumberMin, max: ranges.courseNumberMax },