Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/web/src/lib/components/general/style/ThemePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,24 @@
"Pixel"
]);

const LAST_THEME_KEY = "recal-last-selected-theme";

// Track the last selected theme for "Reset to Theme" functionality
// Load from localStorage on init
let lastSelectedTheme: { name: string; colors: CalColors } | null = null;

// Load last selected theme from localStorage on mount
if (typeof window !== "undefined") {
const stored = localStorage.getItem(LAST_THEME_KEY);
if (stored) {
try {
lastSelectedTheme = JSON.parse(stored);
} catch {
localStorage.removeItem(LAST_THEME_KEY);
}
}
}

/**
* Apply a preset palette
*/
Expand All @@ -50,6 +65,14 @@
calColors.set(hslColors);
lastSelectedTheme = { name, colors: hslColors };

// Persist to localStorage
if (typeof window !== "undefined") {
localStorage.setItem(
LAST_THEME_KEY,
JSON.stringify(lastSelectedTheme)
);
}

// Auto-toggle dark mode for dark palettes
if (DARK_PALETTES.has(name)) {
darkTheme.set(true);
Expand Down Expand Up @@ -82,6 +105,11 @@
calColors.set(DEFAULT_RCARD_COLORS);
darkTheme.set(false);
lastSelectedTheme = null;

// Remove from localStorage
if (typeof window !== "undefined") {
localStorage.removeItem(LAST_THEME_KEY);
}
};

/**
Expand Down
57 changes: 47 additions & 10 deletions apps/web/src/lib/components/recal/Calendar.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { browser } from "$app/environment";
import { currentTerm } from "$lib/changeme";
import {
type BoxParam,
Expand All @@ -22,6 +23,24 @@
import { slide } from "svelte/transition";
import CalBox from "./calendar/CalBox.svelte";

const TIME_MARKS_KEY = "showTimeMarks";

// Load preference from localStorage on init
if (browser) {
const saved = localStorage.getItem(TIME_MARKS_KEY);
if (saved !== null) {
$searchSettings.style["Show Time Marks"] = saved === "true";
}
Comment on lines +29 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid hydrating calendar with mismatched time-mark state

The calendar now reads localStorage and immediately sets $searchSettings.style["Show Time Marks"] during module evaluation. When a user has previously enabled time marks, the client’s initial render includes the extra column while the SSR HTML was generated with the default false value, which causes a hydration mismatch/flash on first load. To keep SSR and client output in sync, load the saved preference in onMount (or delay rendering until after mount) before updating the store.

Useful? React with 👍 / 👎.

}

// Save preference to localStorage when it changes
$: if (browser) {
localStorage.setItem(
TIME_MARKS_KEY,
String($searchSettings.style["Show Time Marks"])
);
}

let toRender: BoxParam[] = [];

let prevSchedule: number = -1;
Expand Down Expand Up @@ -347,22 +366,40 @@
<!--!------------------------------------------------------------------>

<div class="h-full">
<div class="h-full w-full std-area flex rounded-md">
<div class="h-full w-full std-area flex rounded-md relative">
<!-- Time marks toggle button (absolutely positioned) -->
<button
class="absolute top-0 left-0 z-10 h-[4%] w-7 flex items-center justify-center
dark:text-zinc-100 hover:text-zinc-600 hover:dark:text-zinc-300 hover:duration-150"
on:click={() =>
($searchSettings.style["Show Time Marks"] =
!$searchSettings.style["Show Time Marks"])}
title="Toggle time marks">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4">
<path
d="M2 4.5A.5.5 0 012.5 4h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5zm0 5A.5.5 0 012.5 9h4a.5.5 0 010 1h-4a.5.5 0 01-.5-.5zm0 5a.5.5 0 01.5-.5h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5z" />
</svg>
</button>

<!-- Time marks column (only when enabled) -->
{#if $searchSettings.style["Show Time Marks"]}
<div
class="w-10 h-full"
transition:slide={{ axis: "x", duration: 150, easing: linear }}>
class="h-full flex flex-col shrink-0 w-8"
transition:slide={{ axis: "x", duration: 75, easing: linear }}>
<div
class="h-[4%] outline outline-[0.5px] outline-zinc-200
dark:outline-zinc-700 overflow-hidden">
class="h-[4%] outline outline-[0.5px] outline-zinc-200 dark:outline-zinc-700">
</div>
<div class="h-[96%] grid grid-cols-1">
<div class="flex-1 grid grid-cols-1">
{#each MARKERS as marker}
<div
class="text-xs font-light
outline outline-[0.5px] outline-zinc-200
dark:outline-zinc-700 pt-[1px] pl-[1px]
overflow-hidden">
class="text-[10px] font-light
outline outline-[0.5px] outline-zinc-200
dark:outline-zinc-700 pt-[1px] pl-[2px]
overflow-hidden text-zinc-500 dark:text-zinc-400">
{marker}
</div>
{/each}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/components/recal/calendar/CalBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,13 @@
{params.section.title}
</div>

{#if ($searchSettings.style["Always Show Rooms"] || hovered) && isCourseBox(params) && params.section.room}
{#if ($searchSettings.style["Always Show Calendar Box Info"] || hovered) && isCourseBox(params) && params.section.room}
<div class="font-light text-2xs leading-3 pt-[1px]">
{params.section.room}
</div>
{/if}

{#if ($searchSettings.style["Always Show Enrollments"] || hovered) && isCourseBox(params)}
{#if ($searchSettings.style["Always Show Calendar Box Info"] || hovered) && isCourseBox(params)}
<div class="font-light text-2xs leading-3 pt-[1px]">
Enrollment: {params.section.tot}/{params.section.cap === 999
? "∞"
Expand Down
16 changes: 9 additions & 7 deletions apps/web/src/lib/components/recal/left/Saved.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@
</span>
<button
on:click={() => modalStore.push("exportCal")}
class="flex items-center gap-[1px] text-sm">
class="flex items-center gap-1 text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 calbut">
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4 calbut">
<path
fill-rule="evenodd"
d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z"
clip-rule="evenodd" />
stroke-linecap="round"
stroke-linejoin="round"
d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
</svg>

<p class="calbut">Export</p>
Expand Down
83 changes: 73 additions & 10 deletions apps/web/src/lib/components/recal/left/SearchBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@
searchSettings
} from "$lib/stores/recal";
import { sectionData } from "$lib/stores/rsections";
import { getStyles } from "$lib/stores/styles";
import { calColors, darkTheme, getStyles } from "$lib/stores/styles";

// Adjust HSL lightness: positive = darker, negative = lighter
const adjustLightness = (hsl: string, amount: number): string => {
const match = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
if (!match) return hsl;
const [, h, s, l] = match;
const newL = Math.max(0, Math.min(100, parseInt(l) - amount));
return `hsl(${h}, ${s}%, ${newL}%)`;
};
import { toastStore } from "$lib/stores/toast";
import type { SupabaseClient } from "@supabase/supabase-js";
import { getContext } from "svelte";
Expand All @@ -21,6 +30,7 @@
const supabase = getContext("supabase") as SupabaseClient;

let inputBar: HTMLInputElement;
let searchFocused = false;

// Number of results, under which sections are added automatically
const THRESHOLD = 20;
Expand Down Expand Up @@ -55,7 +65,19 @@
sectionData.add(supabase, $currentTerm, $searchResults[i].id);
};

$: cssVarStyles = getStyles("2");
// Re-run when calColors changes (getStyles uses get() internally)
let cssVarStyles: string;
$: $calColors, (cssVarStyles = getStyles("2"));

// Adjust gradient colors: darken in light mode, lighten in dark mode
$: adj = $darkTheme ? -25 : 15;
$: gradColors = [
adjustLightness($calColors["0"], adj),
adjustLightness($calColors["1"], adj),
adjustLightness($calColors["2"], adj),
adjustLightness($calColors["4"], adj),
adjustLightness($calColors["5"], adj)
];
</script>

<div class="flex flex-col justify-between h-16" style={cssVarStyles}>
Expand Down Expand Up @@ -87,9 +109,11 @@
placeholder="Search"
class="search-input std-area rounded-md"
bind:this={inputBar}
on:input={triggerSearch} />
on:input={triggerSearch}
on:focus={() => (searchFocused = true)}
on:blur={() => (searchFocused = false)} />
<button
class="adv-search"
class="adv-search {searchFocused ? 'focused' : ''}"
on:click={() => {
if (!$ready)
toastStore.add("error", "Please wait for the data to load");
Expand All @@ -100,8 +124,38 @@
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6">
stroke={searchFocused ? "url(#theme-gradient)" : "currentColor"}
class="w-6 h-6 gear-icon">
<defs>
<linearGradient
id="theme-gradient"
x1="0%"
y1="0%"
x2="100%"
y2="100%">
<stop offset="0%" stop-color={gradColors[0]}>
<animate
attributeName="stop-color"
values={`${gradColors[0]};${gradColors[1]};${gradColors[2]};${gradColors[3]};${gradColors[4]};${gradColors[0]}`}
dur="3s"
repeatCount="indefinite" />
</stop>
<stop offset="50%" stop-color={gradColors[2]}>
<animate
attributeName="stop-color"
values={`${gradColors[2]};${gradColors[3]};${gradColors[4]};${gradColors[0]};${gradColors[1]};${gradColors[2]}`}
dur="3s"
repeatCount="indefinite" />
</stop>
<stop offset="100%" stop-color={gradColors[4]}>
<animate
attributeName="stop-color"
values={`${gradColors[4]};${gradColors[0]};${gradColors[1]};${gradColors[2]};${gradColors[3]};${gradColors[4]}`}
dur="3s"
repeatCount="indefinite" />
</stop>
</linearGradient>
</defs>
<path
stroke-linecap="round"
stroke-linejoin="round"
Expand All @@ -121,12 +175,21 @@
}

.adv-search {
@apply h-10 w-10 flex justify-center items-center
dark:text-zinc-100;
@apply h-10 w-10 flex justify-center items-center
dark:text-zinc-100;
}

.adv-search:hover svg {
animation: spin 0.8s linear infinite;
}

.adv-search:hover {
@apply text-zinc-600 dark:text-zinc-300 duration-150;
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.togglebutton {
Expand Down
62 changes: 31 additions & 31 deletions apps/web/src/lib/components/recal/left/elements/CourseCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,39 +64,17 @@
}
};

// Color by rating
// Search result styling
if (category === "search") {
styles.stripes = "";
if ($searchSettings.style["Color by Rating"]) {
if (!course.rating) {
styles.color = "hsl(0, 0%, 50%)";
fillStyles();
} else if (course.rating >= 4.5) {
styles.color = "hsl(120, 52%, 75%)";
fillStyles();
} else if (course.rating >= 4.0) {
styles.color = "hsl(197, 34%, 72%)";
fillStyles();
} else if (course.rating >= 3.5) {
styles.color = "hsl(60, 96%, 74%)";
fillStyles();
} else if (course.rating >= 3.0) {
styles.color = "hsl(35, 99%, 65%)";
fillStyles();
} else {
styles.color = "hsl(1, 100%, 69%)";
fillStyles();
}
if ($darkTheme) {
styles.color = "hsl(0, 0%, 10%)";
styles.text = "hsl(0, 0%, 90%)";
styles.hoverColor = "hsl(0, 0%, 10%)";
styles.hoverText = "hsl(0, 0%, 100%)";
} else {
if ($darkTheme) {
styles.color = "hsl(0, 0%, 10%)";
styles.text = "hsl(0, 0%, 90%)";
styles.hoverColor = "hsl(0, 0%, 10%)";
styles.hoverText = "hsl(0, 0%, 100%)";
} else {
styles.color = "hsl(0, 0%,100%)";
fillStyles();
}
styles.color = "hsl(0, 0%,100%)";
fillStyles();
}

// Dynamic color (saved courses)
Expand Down Expand Up @@ -193,9 +171,31 @@
{#if $searchSettings.style["Show Weighted Rating"]}
[{course.adj_rating} adj]
{/if}
{#if $searchSettings.style["Show Enrollment"]}
{@const sections =
$sectionData[$currentTerm]?.[course.id] || []}
{@const priority = ["L", "S", "C", "P", "B", "D", "U"]}
{@const mainCat = priority.find(cat =>
sections.some(s => s.category === cat)
)}
{@const mainSections = mainCat
? sections.filter(s => s.category === mainCat)
: []}
{@const totalEnroll = mainSections.reduce(
(sum, s) => sum + s.tot,
0
)}
{@const totalCap = mainSections.reduce(
(sum, s) => sum + (s.cap !== 999 ? s.cap : 0),
0
)}
Enrollment: {totalCap > 0
? `${totalEnroll}/${totalCap}`
: "N/A"}
{/if}
</div>

{#if $searchSettings.style["Show Instructor(s)"]}
{#if $searchSettings.style["Show Instructors"]}
{#if course.instructors && course.instructors.length > 0}
{#each course.instructors as instructor}
<div class="text-xs italic font-light">
Expand Down
Loading