diff --git a/src/assets/dept_logo/1.png b/src/assets/dept_logo/1.png new file mode 100644 index 00000000..bdea1d2c Binary files /dev/null and b/src/assets/dept_logo/1.png differ diff --git a/src/assets/dept_logo/10.svg b/src/assets/dept_logo/10.svg new file mode 100644 index 00000000..3613eaa6 --- /dev/null +++ b/src/assets/dept_logo/10.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/dept_logo/11.svg b/src/assets/dept_logo/11.svg new file mode 100644 index 00000000..b6c50f9b --- /dev/null +++ b/src/assets/dept_logo/11.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/src/assets/dept_logo/12.webp b/src/assets/dept_logo/12.webp new file mode 100644 index 00000000..8a9a970f Binary files /dev/null and b/src/assets/dept_logo/12.webp differ diff --git a/src/assets/dept_logo/14.png b/src/assets/dept_logo/14.png new file mode 100644 index 00000000..c2eb8ef1 Binary files /dev/null and b/src/assets/dept_logo/14.png differ diff --git a/src/assets/dept_logo/16.svg b/src/assets/dept_logo/16.svg new file mode 100644 index 00000000..da26f9d0 --- /dev/null +++ b/src/assets/dept_logo/16.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/dept_logo/18.png b/src/assets/dept_logo/18.png new file mode 100644 index 00000000..2d03dc05 Binary files /dev/null and b/src/assets/dept_logo/18.png differ diff --git a/src/assets/dept_logo/2.svg b/src/assets/dept_logo/2.svg new file mode 100644 index 00000000..de6ce585 --- /dev/null +++ b/src/assets/dept_logo/2.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/dept_logo/20.png b/src/assets/dept_logo/20.png new file mode 100644 index 00000000..67e99e67 Binary files /dev/null and b/src/assets/dept_logo/20.png differ diff --git a/src/assets/dept_logo/21A+21H+STS.png b/src/assets/dept_logo/21A+21H+STS.png new file mode 100644 index 00000000..7c835926 Binary files /dev/null and b/src/assets/dept_logo/21A+21H+STS.png differ diff --git a/src/assets/dept_logo/21L.png b/src/assets/dept_logo/21L.png new file mode 100644 index 00000000..b6392348 Binary files /dev/null and b/src/assets/dept_logo/21L.png differ diff --git a/src/assets/dept_logo/3.svg b/src/assets/dept_logo/3.svg new file mode 100644 index 00000000..de3c020c --- /dev/null +++ b/src/assets/dept_logo/3.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/src/assets/dept_logo/5.png b/src/assets/dept_logo/5.png new file mode 100644 index 00000000..be2ff652 Binary files /dev/null and b/src/assets/dept_logo/5.png differ diff --git a/src/assets/dept_logo/6.svg b/src/assets/dept_logo/6.svg new file mode 100644 index 00000000..cc4479e8 --- /dev/null +++ b/src/assets/dept_logo/6.svg @@ -0,0 +1,2 @@ + +Group \ No newline at end of file diff --git a/src/assets/dept_logo/8.svg b/src/assets/dept_logo/8.svg new file mode 100644 index 00000000..2be4d7db --- /dev/null +++ b/src/assets/dept_logo/8.svg @@ -0,0 +1 @@ + diff --git a/src/assets/dept_logo/9.png b/src/assets/dept_logo/9.png new file mode 100644 index 00000000..f6dbeddc Binary files /dev/null and b/src/assets/dept_logo/9.png differ diff --git a/src/assets/dept_logo/CC.png b/src/assets/dept_logo/CC.png new file mode 100644 index 00000000..1638a1fd Binary files /dev/null and b/src/assets/dept_logo/CC.png differ diff --git a/src/assets/dept_logo/CMS+21W.png b/src/assets/dept_logo/CMS+21W.png new file mode 100644 index 00000000..83aa186a Binary files /dev/null and b/src/assets/dept_logo/CMS+21W.png differ diff --git a/src/assets/dept_logo/CSB.png b/src/assets/dept_logo/CSB.png new file mode 100644 index 00000000..2091aedc Binary files /dev/null and b/src/assets/dept_logo/CSB.png differ diff --git a/src/assets/dept_logo/CSE.png b/src/assets/dept_logo/CSE.png new file mode 100644 index 00000000..aa781a88 Binary files /dev/null and b/src/assets/dept_logo/CSE.png differ diff --git a/src/assets/dept_logo/EC.svg b/src/assets/dept_logo/EC.svg new file mode 100644 index 00000000..6975200b --- /dev/null +++ b/src/assets/dept_logo/EC.svg @@ -0,0 +1,54 @@ + + + + + + + + + diff --git a/src/assets/dept_logo/HST.svg b/src/assets/dept_logo/HST.svg new file mode 100644 index 00000000..5e79e5d6 --- /dev/null +++ b/src/assets/dept_logo/HST.svg @@ -0,0 +1,72 @@ + + + + + Harvard-MIT Health Sciences and Technology + + + + + + + + + diff --git a/src/assets/dept_logo/IDS.png b/src/assets/dept_logo/IDS.png new file mode 100644 index 00000000..2247eb82 Binary files /dev/null and b/src/assets/dept_logo/IDS.png differ diff --git a/src/assets/dept_logo/MAS.png b/src/assets/dept_logo/MAS.png new file mode 100644 index 00000000..6deb6aba Binary files /dev/null and b/src/assets/dept_logo/MAS.png differ diff --git a/src/assets/dept_logo/SCM.png b/src/assets/dept_logo/SCM.png new file mode 100644 index 00000000..d7d2372d Binary files /dev/null and b/src/assets/dept_logo/SCM.png differ diff --git a/src/assets/dept_logo/STS.png b/src/assets/dept_logo/STS.png new file mode 100644 index 00000000..7ca1ba2a Binary files /dev/null and b/src/assets/dept_logo/STS.png differ diff --git a/src/assets/dept_logo/WGS.png b/src/assets/dept_logo/WGS.png new file mode 100644 index 00000000..e1f24128 Binary files /dev/null and b/src/assets/dept_logo/WGS.png differ diff --git a/src/components/ActivityDescription.module.scss b/src/components/ActivityDescription.module.scss new file mode 100644 index 00000000..01a25d30 --- /dev/null +++ b/src/components/ActivityDescription.module.scss @@ -0,0 +1,73 @@ +#class_description { + position: relative; + + &::before { + content: ""; + background-image: var(--dept_logo_src); + height: 60vh; + display: block; + + z-index: -100000; + background-repeat: no-repeat; + background-size: min(60vw, 70vh); + background-position: center; + + @media (min-width: 1024px) { + position: fixed; + width: max(60vw, 60vh); + bottom: -5em; + right: -5em; + } + + @media (max-width: 1023px) { + position: absolute; + top: -5em; + left: 0; + right: 0; + } + } + + &.sharpen::before { + @media (max-width: 1023px) { + background-size: 100vw; + height: unset; + bottom: -10em; + } + + @media (min-width: 1024px) { + background-size: 110%; + background-position: top; + position: absolute; + left: -2em; + right: 0; + top: -5em; + width: unset; + } + } + + p { + width: fit-content; + backdrop-filter: blur(2em); + } +} + + +html:global(.light) #class_description::before { + opacity: 0.05; + filter: saturate(5) brightness(0.5); +} + + +html:global(.dark) #class_description:not(.sharpen):not(.lighten)::before { + opacity: 0.10; + filter: blur(0.5em); +} + +html:global(.dark) #class_description.sharpen::before { + opacity: 0.15; +} + +html:global(.dark) #class_description.lighten::before { + opacity: 0.15; + filter: blur(0.5em) invert(1) brightness(0.8) invert(1); +} \ No newline at end of file diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index 54148bd6..57521386 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -13,6 +13,36 @@ import { linkClasses } from "../lib/utils"; import { ClassButtons, NonClassButtons } from "./ActivityButtons"; import { LuExternalLink } from "react-icons/lu"; +import styles from "./ActivityDescription.module.scss"; +import type { CSSProperties } from "react"; + +import dept_logo_1 from "../assets/dept_logo/1.png"; +import dept_logo_2 from "../assets/dept_logo/2.svg"; +import dept_logo_3 from "../assets/dept_logo/3.svg"; +import dept_logo_5 from "../assets/dept_logo/5.png"; +import dept_logo_6 from "../assets/dept_logo/6.svg"; +import dept_logo_8 from "../assets/dept_logo/8.svg"; +import dept_logo_9 from "../assets/dept_logo/9.png"; +import dept_logo_10 from "../assets/dept_logo/10.svg"; +import dept_logo_11 from "../assets/dept_logo/11.svg"; +import dept_logo_12 from "../assets/dept_logo/12.webp"; +import dept_logo_14 from "../assets/dept_logo/14.png"; +import dept_logo_16 from "../assets/dept_logo/16.svg"; +import dept_logo_18 from "../assets/dept_logo/18.png"; +import dept_logo_20 from "../assets/dept_logo/20.png"; +import dept_logo_21A_21H_STS from "../assets/dept_logo/21A+21H+STS.png"; +import dept_logo_21L from "../assets/dept_logo/21L.png"; +import dept_logo_CC from "../assets/dept_logo/CC.png"; +import dept_logo_CMS_21W from "../assets/dept_logo/CMS+21W.png"; +import dept_logo_CSB from "../assets/dept_logo/CSB.png"; +import dept_logo_CSE from "../assets/dept_logo/CSE.png"; +import dept_logo_EC from "../assets/dept_logo/EC.svg"; +import dept_logo_HST from "../assets/dept_logo/HST.svg"; +import dept_logo_IDS from "../assets/dept_logo/IDS.png"; +import dept_logo_MAS from "../assets/dept_logo/MAS.png"; +import dept_logo_SCM from "../assets/dept_logo/SCM.png"; +import dept_logo_WGS from "../assets/dept_logo/WGS.png"; + /** A small image indicating a flag, like Spring or CI-H. */ function TypeSpan(props: { flag?: keyof Flags; title: string }) { const { flag, title } = props; @@ -204,8 +234,58 @@ function ClassBody(props: { cls: Class; state: State }) { function ClassDescription(props: { cls: Class; state: State }) { const { cls, state } = props; + const showLogo = + typeof state.decoScheme.showDepartmentLogo === "boolean" + ? state.decoScheme.showDepartmentLogo + : state.decoScheme.showDepartmentLogo[state.colorScheme.id]; + const departmentLogo = ((file) => + file == null || !showLogo ? "none" : `url("${file}")`)( + { + "1": dept_logo_1, + "2": dept_logo_2, + "3": dept_logo_3, + "5": dept_logo_5, + "6": dept_logo_6, + "8": dept_logo_8, + "9": dept_logo_9, + "10": dept_logo_10, + "11": dept_logo_11, + "12": dept_logo_12, + "14": dept_logo_14, + "16": dept_logo_16, + "18": dept_logo_18, + "20": dept_logo_20, + "21A": dept_logo_21A_21H_STS, + "21H": dept_logo_21A_21H_STS, + STS: dept_logo_21A_21H_STS, + "21L": dept_logo_21L, + CC: dept_logo_CC, + CMS: dept_logo_CMS_21W, + "21W": dept_logo_CMS_21W, + CSB: dept_logo_CSB, + CSE: dept_logo_CSE, + EC: dept_logo_EC, + HST: dept_logo_HST, + IDS: dept_logo_IDS, + MAS: dept_logo_MAS, + SCM: dept_logo_SCM, + WGS: dept_logo_WGS, + }[cls.course], + ); + const className = { + "18": styles.sharpen, + "5": styles.lighten, + "11": styles.lighten, + }[cls.course]; + return ( - + {cls.number}: {cls.name} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 09b5bb4d..a176fbf4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -25,7 +25,7 @@ import { createListCollection } from "@chakra-ui/react"; import type { State } from "../lib/state"; import { useState, useRef } from "react"; -import { COLOR_SCHEME_PRESETS } from "../lib/colors"; +import { COLOR_SCHEME_PRESETS, DECORATION_SCHEME_PRESETS } from "../lib/colors"; import type { Preferences } from "../lib/schema"; import { DEFAULT_PREFERENCES } from "../lib/schema"; @@ -117,6 +117,34 @@ export function PreferencesDialog(props: { ))} + ({ + label: name, + value: name, + })), + })} + value={[preferences.decoScheme.name]} + onValueChange={(e) => { + const decoScheme = DECORATION_SCHEME_PRESETS.find( + ({ name }) => name === e.value[0], + ); + if (!decoScheme) return; + previewPreferences({ ...preferences, decoScheme }); + }} + > + Decorations: + + + + + {DECORATION_SCHEME_PRESETS.map(({ name }) => ( + + {name} + + ))} + + diff --git a/src/components/SelectedActivities.tsx b/src/components/SelectedActivities.tsx index d24aaf91..7ee3b97c 100644 --- a/src/components/SelectedActivities.tsx +++ b/src/components/SelectedActivities.tsx @@ -2,7 +2,7 @@ import { Flex, Text, Button, ButtonGroup } from "@chakra-ui/react"; import type { ComponentPropsWithoutRef } from "react"; import type { Activity } from "../lib/activity"; -import { textColor } from "../lib/colors"; +import { multiplyColor, textColor } from "../lib/colors"; import { Class } from "../lib/class"; import type { State } from "../lib/state"; @@ -18,6 +18,10 @@ export function ColorButton( backgroundColor={color} borderColor={color} color={textColor(color)} + _hover={{ + backgroundColor: multiplyColor(color, 1.1), + color: textColor(multiplyColor(color, 1.1)), + }} style={{ ...style, }} diff --git a/src/lib/colors.ts b/src/lib/colors.ts index fdfd2164..a201ab02 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -3,12 +3,14 @@ import type { Activity } from "./activity"; /** The type of color schemes. */ export interface ColorScheme { + id: string; name: string; colorMode: ColorMode; backgroundColors: string[]; } const classic: ColorScheme = { + id: "light", name: "Classic", colorMode: "light", backgroundColors: [ @@ -26,6 +28,7 @@ const classic: ColorScheme = { }; const classicDark: ColorScheme = { + id: "dark", name: "Classic (Dark)", colorMode: "dark", backgroundColors: [ @@ -43,6 +46,7 @@ const classicDark: ColorScheme = { }; const highContrast: ColorScheme = { + id: "light_hc", name: "High Contrast", colorMode: "light", backgroundColors: [ @@ -58,6 +62,7 @@ const highContrast: ColorScheme = { }; const highContrastDark: ColorScheme = { + id: "dark_hc", name: "High Contrast (Dark)", colorMode: "dark", backgroundColors: [ @@ -80,6 +85,41 @@ export const COLOR_SCHEME_PRESETS: ColorScheme[] = [ highContrastDark, ]; +/** The type of decoration scheme */ +export interface DecorationScheme { + id: string; + name: string; + showDepartmentLogo: boolean | Record; +} + +const decoEnabled: DecorationScheme = { + id: "deco_enable", + name: "Enabled", + showDepartmentLogo: true, +}; +const decoDisabled: DecorationScheme = { + id: "deco_disable", + name: "Disabled", + showDepartmentLogo: false, +}; +const decoDefault: DecorationScheme = { + id: "deco_default", + name: "Auto", + showDepartmentLogo: { + light: true, + dark: true, + light_hc: false, + dark_hc: false, + }, +}; + +/** The default decoration schemes. */ +export const DECORATION_SCHEME_PRESETS: DecorationScheme[] = [ + decoDefault, + decoEnabled, + decoDisabled, +]; + /** The default background color for a color scheme. */ export function fallbackColor(colorScheme: ColorScheme): string { return colorScheme.colorMode === "light" ? "#4A5568" : "#CBD5E0"; @@ -124,14 +164,43 @@ export function chooseColors( } } +export function parseColor(color: string): { r: number; g: number; b: number } { + if (color.startsWith("#")) { + const r = parseInt(color.substring(1, 3), 16); + const g = parseInt(color.substring(3, 5), 16); + const b = parseInt(color.substring(5, 7), 16); + return { r, g, b }; + } + if (color.startsWith("rgb")) { + const [_, r_str, g_str, b_str] = + /rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)/.exec(color) ?? []; + const [r, g, b] = [parseInt(r_str), parseInt(g_str), parseInt(b_str)]; + return { r, g, b }; + } + console.warn("invalid color:", color); + return { r: 0, g: 0, b: 0 }; +} + /** Choose a text color for a background given by hex code color. */ export function textColor(color: string): string { - const r = parseInt(color.substring(1, 3), 16); - const g = parseInt(color.substring(3, 5), 16); - const b = parseInt(color.substring(5, 7), 16); + const { r, g, b } = parseColor(color); const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 ? "#000000" : "#ffffff"; } +/** Multiply a given hex code color by a 0-1 factor. */ +export function multiplyColor(color: string, multiplier: number): string { + const { r, g, b } = parseColor(color); + const to_hex = (v: number) => + Math.max(0, Math.min(255, Math.floor(v))) + .toString(16) + .padStart(2, "0"); + return ( + "#" + + to_hex(r * multiplier) + + to_hex(g * multiplier) + + to_hex(b * multiplier) + ); +} /** Return a standard #AABBCC representation from an input color */ export function canonicalizeColor(code: string): string | undefined { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 43be2db9..552abd41 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,6 +1,6 @@ import type { Activity } from "./activity"; -import type { ColorScheme } from "./colors"; -import { COLOR_SCHEME_PRESETS } from "./colors"; +import type { ColorScheme, DecorationScheme } from "./colors"; +import { COLOR_SCHEME_PRESETS, DECORATION_SCHEME_PRESETS } from "./colors"; /** A save has an ID and a name. */ export interface Save { @@ -11,15 +11,50 @@ export interface Save { /** Browser-specific user preferences. */ export interface Preferences { colorScheme: ColorScheme; + decoScheme: DecorationScheme; roundedCorners: boolean; showEventTimes: boolean; defaultScheduleId: string | null; showFeedback: boolean; } +export function ensurePreferencesValid( + prefs: Partial, +): Preferences { + return { + colorScheme: + COLOR_SCHEME_PRESETS.find((v) => v.id === prefs.colorScheme?.id) ?? + COLOR_SCHEME_PRESETS.find((v) => v.name === prefs.colorScheme?.name) ?? + DEFAULT_PREFERENCES.colorScheme, + decoScheme: + DECORATION_SCHEME_PRESETS.find((v) => v.id === prefs.decoScheme?.id) ?? + DECORATION_SCHEME_PRESETS.find( + (v) => v.name === prefs.decoScheme?.name, + ) ?? + DEFAULT_PREFERENCES.decoScheme, + roundedCorners: + typeof prefs.roundedCorners == "boolean" + ? prefs.roundedCorners + : DEFAULT_PREFERENCES.roundedCorners, + showEventTimes: + typeof prefs.showEventTimes == "boolean" + ? prefs.showEventTimes + : DEFAULT_PREFERENCES.showEventTimes, + defaultScheduleId: + typeof prefs.defaultScheduleId == "number" + ? prefs.defaultScheduleId + : DEFAULT_PREFERENCES.defaultScheduleId, + showFeedback: + typeof prefs.showFeedback == "boolean" + ? prefs.showFeedback + : DEFAULT_PREFERENCES.showFeedback, + }; +} + /** The default user preferences. */ export const DEFAULT_PREFERENCES: Preferences = { colorScheme: COLOR_SCHEME_PRESETS[0], + decoScheme: DECORATION_SCHEME_PRESETS[0], roundedCorners: false, showEventTimes: false, defaultScheduleId: null, diff --git a/src/lib/state.ts b/src/lib/state.ts index b6e023a7..0b0aab78 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -6,13 +6,13 @@ import { scheduleSlots } from "./calendarSlots"; import type { Section, SectionLockOption, Sections } from "./class"; import { Class } from "./class"; import type { Term } from "./dates"; -import type { ColorScheme } from "./colors"; +import type { ColorScheme, DecorationScheme } from "./colors"; import { chooseColors, fallbackColor } from "./colors"; import type { RawClass, RawTimeslot } from "./rawClass"; import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; -import { DEFAULT_PREFERENCES } from "./schema"; +import { DEFAULT_PREFERENCES, ensurePreferencesValid } from "./schema"; /** * Global State object. Maintains global program state (selected classes, @@ -85,6 +85,10 @@ export class State { get colorScheme(): ColorScheme { return this.preferences.colorScheme; } + /** The color scheme. */ + get decoScheme(): DecorationScheme { + return this.preferences.decoScheme; + } //======================================================================== // Activity handlers @@ -453,7 +457,7 @@ export class State { initState(): void { const preferences = this.store.globalGet("preferences"); if (preferences) { - this.preferences = preferences; + this.preferences = ensurePreferencesValid(preferences); } const url = new URL(window.location.href); const save = url.searchParams.get("s");