From f045b400a56569f7353eb00ecd0d081b68660fc5 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 8 Feb 2024 11:52:29 -0600 Subject: [PATCH] feat: PopupCourseBlock Component (#79) Co-authored-by: Razboy20 Co-authored-by: Razboy20 --- package.json | 1 + pnpm-lock.yaml | 7 ++ src/shared/util/colors.ts | 50 ++++++++ .../components/PopupCourseBlock.stories.tsx | 118 ++++++++++++++++++ .../common/ExtensionRoot/ExtensionRoot.tsx | 1 + .../PopupCourseBlock/PopupCourseBlock.tsx | 61 +++++++++ .../components/common/Text/Text.module.scss | 104 +++++++-------- 7 files changed, 291 insertions(+), 51 deletions(-) create mode 100644 src/shared/util/colors.ts create mode 100644 src/stories/components/PopupCourseBlock.stories.tsx create mode 100644 src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx diff --git a/package.json b/package.json index fd7ab2a6..1ed328d0 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@unocss/postcss": "^0.58.4", "@unocss/preset-uno": "^0.58.4", "@unocss/preset-web-fonts": "^0.58.4", + "@unocss/reset": "^0.58.5", "@unocss/transformer-directives": "^0.58.4", "@unocss/transformer-variant-group": "^0.58.4", "@vitejs/plugin-react-swc": "^3.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5afb4a9..4f621764 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,9 @@ devDependencies: '@unocss/preset-web-fonts': specifier: ^0.58.4 version: 0.58.4 + '@unocss/reset': + specifier: ^0.58.5 + version: 0.58.5 '@unocss/transformer-directives': specifier: ^0.58.4 version: 0.58.4 @@ -5046,6 +5049,10 @@ packages: resolution: {integrity: sha512-ZZTrAdl4WWmMjQdOqcOSWdgFH6kdFKZjPu4c6Ijxk7KvY2BW3nttTTBa7IYeuXFHVfcExUFqlOgRurt+NeWYyQ==} dev: true + /@unocss/reset@0.58.5: + resolution: {integrity: sha512-2wMrkCj3SSb5hrx9TKs5jZa34QIRkHv9FotbJutAPo7o8hx+XXn56ogzdoUrcFPJZJUx2R2nyOVbSlGMIjtFtw==} + dev: true + /@unocss/rule-utils@0.58.4: resolution: {integrity: sha512-52Jp4I+joGTaDm7ehB/7uZ2kJL+9BZcYRDUVk4IDacDH5W9yxf1F75LzYT8jJVWXD/HIhiS0r9V6qhcBq2OWZw==} engines: {node: '>=14'} diff --git a/src/shared/util/colors.ts b/src/shared/util/colors.ts new file mode 100644 index 00000000..807a2c16 --- /dev/null +++ b/src/shared/util/colors.ts @@ -0,0 +1,50 @@ +import { theme } from 'unocss/preset-mini'; + +export interface CourseColors { + primaryColor: string; + secondaryColor: string; +} + +// calculates luminance of a hex string +function getLuminance(hex: string): number { + let r = parseInt(hex.substring(1, 3), 16); + let g = parseInt(hex.substring(3, 5), 16); + let b = parseInt(hex.substring(5, 7), 16); + + [r, g, b] = [r, g, b].map(color => { + let c = color / 255; + + c = c > 0.03928 ? ((c + 0.055) / 1.055) ** 2.4 : (c /= 12.92); + + return c; + }); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +// calculates contrast ratio between two hex strings +function contrastRatioPair(hex1: string, hex2: string) { + const lum1 = getLuminance(hex1); + const lum2 = getLuminance(hex2); + + return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05); +} + +/** + * Generate a tailwind classname for the font color based on the background color + * @param bgColor the tailwind classname for background ex. "bg-emerald-500" + */ +export function pickFontColor(bgColor: string): 'text-white' | 'text-black' { + return contrastRatioPair(bgColor, '#606060') > contrastRatioPair(bgColor, '#ffffff') ? 'text-black' : 'text-white'; +} + +/** + * Get primary and secondary colors from a tailwind colorway + * @param colorway the tailwind colorway ex. "emerald" + */ +export function getCourseColors(colorway: keyof typeof theme.colors): CourseColors { + return { + primaryColor: theme.colors[colorway][600] as string, + secondaryColor: theme.colors[colorway][800] as string, + }; +} diff --git a/src/stories/components/PopupCourseBlock.stories.tsx b/src/stories/components/PopupCourseBlock.stories.tsx new file mode 100644 index 00000000..636617f5 --- /dev/null +++ b/src/stories/components/PopupCourseBlock.stories.tsx @@ -0,0 +1,118 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Course, Status } from 'src/shared/types/Course'; +import { CourseMeeting } from 'src/shared/types/CourseMeeting'; +import Instructor from 'src/shared/types/Instructor'; +import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock'; +import { getCourseColors } from 'src/shared/util/colors'; +import { theme } from 'unocss/preset-mini'; + +const exampleCourse: Course = new Course({ + courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB', + creditHours: 3, + department: 'C S', + description: [ + 'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.', + 'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.', + 'May be counted toward the Quantitative Reasoning flag requirement.', + 'Designed to accommodate 100 or more students.', + 'Taught as a Web-based course.', + ], + flags: ['Quantitative Reasoning'], + fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB', + instructionMode: 'Online', + instructors: [ + new Instructor({ + firstName: 'Bevo', + lastName: 'Bevo', + fullName: 'Bevo Bevo', + }), + ], + isReserved: false, + number: '303E', + schedule: { + meetings: [ + new CourseMeeting({ + days: ['Tuesday', 'Thursday'], + endTime: 660, + startTime: 570, + }), + ], + }, + semester: { + code: '12345', + season: 'Spring', + year: 2024, + }, + status: Status.WAITLISTED, + uniqueId: 12345, + url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/', +}); + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/Common/PopupCourseBlock', + component: PopupCourseBlock, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + args: { + colors: getCourseColors('emerald'), + course: exampleCourse, + }, + argTypes: { + colors: { + description: 'the colors to use for the course block', + control: 'object', + }, + course: { + description: 'the course to show data for', + control: 'object', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Default: Story = { + args: {}, +}; + +export const Variants: Story = { + render: props => ( +
+ + + + +
+ ), +}; + +const colors = Object.keys(theme.colors) + // check that the color is a colorway (is an object) + .filter(color => typeof theme.colors[color] === 'object') + .slice(0, 17) + .map(color => getCourseColors(color as keyof typeof theme.colors)); + +export const AllColors: Story = { + render: props => ( +
+ {colors.map((color, i) => ( + + ))} +
+ ), + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1046-6714&mode=design&t=5Bjr7qGHNXmjfMTc-0', + }, + }, +}; diff --git a/src/views/components/common/ExtensionRoot/ExtensionRoot.tsx b/src/views/components/common/ExtensionRoot/ExtensionRoot.tsx index 9bc27972..bb2fd081 100644 --- a/src/views/components/common/ExtensionRoot/ExtensionRoot.tsx +++ b/src/views/components/common/ExtensionRoot/ExtensionRoot.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from './ExtensionRoot.module.scss'; +import '@unocss/reset/tailwind-compat.css'; import 'uno.css'; interface Props { diff --git a/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx b/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx new file mode 100644 index 00000000..d1389649 --- /dev/null +++ b/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx @@ -0,0 +1,61 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; +import { Course, Status } from '@shared/types/Course'; +import { StatusIcon } from '@shared/util/icons'; +import { CourseColors, getCourseColors, pickFontColor } from '@shared/util/colors'; +import DragIndicatorIcon from '~icons/material-symbols/drag-indicator'; +import Text from '../Text/Text'; + +/** + * Props for PopupCourseBlock + */ +export interface PopupCourseBlockProps { + className?: string; + course: Course; + colors: CourseColors; +} + +/** + * The "course block" to be used in the extension popup. + * + * @param props PopupCourseBlockProps + */ +export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element { + // whiteText based on secondaryColor + const fontColor = pickFontColor(colors.primaryColor); + + return ( +
+
+ +
+ + {course.uniqueId} {course.department} {course.number} –{' '} + {course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)} + + {course.status !== Status.OPEN && ( +
+ +
+ )} +
+ ); +} diff --git a/src/views/components/common/Text/Text.module.scss b/src/views/components/common/Text/Text.module.scss index 1f52a96b..16f52b29 100644 --- a/src/views/components/common/Text/Text.module.scss +++ b/src/views/components/common/Text/Text.module.scss @@ -1,65 +1,67 @@ @use 'src/views/styles/colors.module.scss'; @use 'src/views/styles/fonts.module.scss'; -.text { - font-family: 'Roboto Flex', sans-serif; - line-height: normal; - font-style: normal; -} +@layer theme { + .text { + font-family: 'Roboto Flex', sans-serif; + line-height: normal; + font-style: normal; + } -.mini { - font-size: 0.79rem; - font-weight: 500; -} + .mini { + font-size: 0.79rem; + font-weight: 500; + } -.small { - font-size: 0.88875rem; - font-weight: 500; -} + .small { + font-size: 0.88875rem; + font-weight: 500; + } -.p { - font-size: 1rem; - font-weight: 400; - letter-spacing: 0.025rem; -} + .p { + font-size: 1rem; + font-weight: 400; + letter-spacing: 0.025rem; + } -.h4 { - font-size: 1.125rem; - font-weight: 500; -} + .h4 { + font-size: 1.125rem; + font-weight: 500; + } -.h3-course { - font-size: 0.6875rem; - font-weight: 400; - line-height: 100%; /* 0.6875rem */ -} + .h3-course { + font-size: 0.6875rem; + font-weight: 400; + line-height: 100%; /* 0.6875rem */ + } -.h3 { - font-size: 1.26563rem; - font-weight: 600; - text-transform: uppercase; -} + .h3 { + font-size: 1.26563rem; + font-weight: 600; + text-transform: uppercase; + } -.h2-course { - font-size: 1rem; - font-weight: 500; - letter-spacing: 0.03125rem; - text-transform: capitalize; -} + .h2-course { + font-size: 1rem; + font-weight: 500; + letter-spacing: 0.03125rem; + text-transform: capitalize; + } -.h2 { - font-size: 1.42375rem; - font-weight: 500; -} + .h2 { + font-size: 1.42375rem; + font-weight: 500; + } -.h1-course { - font-size: 1rem; - font-weight: 600; - text-transform: capitalize; -} + .h1-course { + font-size: 1rem; + font-weight: 600; + text-transform: capitalize; + } -.h1 { - font-size: 1.60188rem; - font-weight: 700; - text-transform: uppercase; + .h1 { + font-size: 1.60188rem; + font-weight: 700; + text-transform: uppercase; + } }