Skip to content

Commit f045b40

Browse files
feat: PopupCourseBlock Component (Longhorn-Developers#79)
Co-authored-by: Razboy20 <[email protected]> Co-authored-by: Razboy20 <[email protected]>
1 parent dd2f696 commit f045b40

File tree

7 files changed

+291
-51
lines changed

7 files changed

+291
-51
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@unocss/postcss": "^0.58.4",
5656
"@unocss/preset-uno": "^0.58.4",
5757
"@unocss/preset-web-fonts": "^0.58.4",
58+
"@unocss/reset": "^0.58.5",
5859
"@unocss/transformer-directives": "^0.58.4",
5960
"@unocss/transformer-variant-group": "^0.58.4",
6061
"@vitejs/plugin-react-swc": "^3.6.0",

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/shared/util/colors.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { theme } from 'unocss/preset-mini';
2+
3+
export interface CourseColors {
4+
primaryColor: string;
5+
secondaryColor: string;
6+
}
7+
8+
// calculates luminance of a hex string
9+
function getLuminance(hex: string): number {
10+
let r = parseInt(hex.substring(1, 3), 16);
11+
let g = parseInt(hex.substring(3, 5), 16);
12+
let b = parseInt(hex.substring(5, 7), 16);
13+
14+
[r, g, b] = [r, g, b].map(color => {
15+
let c = color / 255;
16+
17+
c = c > 0.03928 ? ((c + 0.055) / 1.055) ** 2.4 : (c /= 12.92);
18+
19+
return c;
20+
});
21+
22+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
23+
}
24+
25+
// calculates contrast ratio between two hex strings
26+
function contrastRatioPair(hex1: string, hex2: string) {
27+
const lum1 = getLuminance(hex1);
28+
const lum2 = getLuminance(hex2);
29+
30+
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
31+
}
32+
33+
/**
34+
* Generate a tailwind classname for the font color based on the background color
35+
* @param bgColor the tailwind classname for background ex. "bg-emerald-500"
36+
*/
37+
export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
38+
return contrastRatioPair(bgColor, '#606060') > contrastRatioPair(bgColor, '#ffffff') ? 'text-black' : 'text-white';
39+
}
40+
41+
/**
42+
* Get primary and secondary colors from a tailwind colorway
43+
* @param colorway the tailwind colorway ex. "emerald"
44+
*/
45+
export function getCourseColors(colorway: keyof typeof theme.colors): CourseColors {
46+
return {
47+
primaryColor: theme.colors[colorway][600] as string,
48+
secondaryColor: theme.colors[colorway][800] as string,
49+
};
50+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import React from 'react';
3+
import { Course, Status } from 'src/shared/types/Course';
4+
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
5+
import Instructor from 'src/shared/types/Instructor';
6+
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
7+
import { getCourseColors } from 'src/shared/util/colors';
8+
import { theme } from 'unocss/preset-mini';
9+
10+
const exampleCourse: Course = new Course({
11+
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
12+
creditHours: 3,
13+
department: 'C S',
14+
description: [
15+
'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.',
16+
'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.',
17+
'May be counted toward the Quantitative Reasoning flag requirement.',
18+
'Designed to accommodate 100 or more students.',
19+
'Taught as a Web-based course.',
20+
],
21+
flags: ['Quantitative Reasoning'],
22+
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
23+
instructionMode: 'Online',
24+
instructors: [
25+
new Instructor({
26+
firstName: 'Bevo',
27+
lastName: 'Bevo',
28+
fullName: 'Bevo Bevo',
29+
}),
30+
],
31+
isReserved: false,
32+
number: '303E',
33+
schedule: {
34+
meetings: [
35+
new CourseMeeting({
36+
days: ['Tuesday', 'Thursday'],
37+
endTime: 660,
38+
startTime: 570,
39+
}),
40+
],
41+
},
42+
semester: {
43+
code: '12345',
44+
season: 'Spring',
45+
year: 2024,
46+
},
47+
status: Status.WAITLISTED,
48+
uniqueId: 12345,
49+
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
50+
});
51+
52+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
53+
const meta = {
54+
title: 'Components/Common/PopupCourseBlock',
55+
component: PopupCourseBlock,
56+
parameters: {
57+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
58+
layout: 'centered',
59+
},
60+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
61+
tags: ['autodocs'],
62+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
63+
args: {
64+
colors: getCourseColors('emerald'),
65+
course: exampleCourse,
66+
},
67+
argTypes: {
68+
colors: {
69+
description: 'the colors to use for the course block',
70+
control: 'object',
71+
},
72+
course: {
73+
description: 'the course to show data for',
74+
control: 'object',
75+
},
76+
},
77+
} satisfies Meta<typeof PopupCourseBlock>;
78+
79+
export default meta;
80+
type Story = StoryObj<typeof meta>;
81+
82+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
83+
export const Default: Story = {
84+
args: {},
85+
};
86+
87+
export const Variants: Story = {
88+
render: props => (
89+
<div className='grid grid-cols-2 max-w-2xl w-90vw gap-x-4 gap-y-2'>
90+
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.OPEN })} />
91+
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CLOSED })} />
92+
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.WAITLISTED })} />
93+
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CANCELLED })} />
94+
</div>
95+
),
96+
};
97+
98+
const colors = Object.keys(theme.colors)
99+
// check that the color is a colorway (is an object)
100+
.filter(color => typeof theme.colors[color] === 'object')
101+
.slice(0, 17)
102+
.map(color => getCourseColors(color as keyof typeof theme.colors));
103+
104+
export const AllColors: Story = {
105+
render: props => (
106+
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
107+
{colors.map((color, i) => (
108+
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
109+
))}
110+
</div>
111+
),
112+
parameters: {
113+
design: {
114+
type: 'figma',
115+
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1046-6714&mode=design&t=5Bjr7qGHNXmjfMTc-0',
116+
},
117+
},
118+
};

src/views/components/common/ExtensionRoot/ExtensionRoot.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import styles from './ExtensionRoot.module.scss';
33

4+
import '@unocss/reset/tailwind-compat.css';
45
import 'uno.css';
56

67
interface Props {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import clsx from 'clsx';
2+
import React, { useState } from 'react';
3+
import { Course, Status } from '@shared/types/Course';
4+
import { StatusIcon } from '@shared/util/icons';
5+
import { CourseColors, getCourseColors, pickFontColor } from '@shared/util/colors';
6+
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
7+
import Text from '../Text/Text';
8+
9+
/**
10+
* Props for PopupCourseBlock
11+
*/
12+
export interface PopupCourseBlockProps {
13+
className?: string;
14+
course: Course;
15+
colors: CourseColors;
16+
}
17+
18+
/**
19+
* The "course block" to be used in the extension popup.
20+
*
21+
* @param props PopupCourseBlockProps
22+
*/
23+
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
24+
// whiteText based on secondaryColor
25+
const fontColor = pickFontColor(colors.primaryColor);
26+
27+
return (
28+
<div
29+
style={{
30+
backgroundColor: colors.primaryColor,
31+
}}
32+
className={clsx('h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3', className)}
33+
>
34+
<div
35+
style={{
36+
backgroundColor: colors.secondaryColor,
37+
}}
38+
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
39+
>
40+
<DragIndicatorIcon className='h-6 w-6 text-white' />
41+
</div>
42+
<Text
43+
className={clsx('flex-1 py-3.5 text-ellipsis whitespace-nowrap overflow-hidden', fontColor)}
44+
variant='h1-course'
45+
>
46+
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} &ndash;{' '}
47+
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
48+
</Text>
49+
{course.status !== Status.OPEN && (
50+
<div
51+
style={{
52+
backgroundColor: colors.secondaryColor,
53+
}}
54+
className='ml-1 flex items-center justify-center justify-self-end rounded p-1px text-white'
55+
>
56+
<StatusIcon status={course.status} className='h-5 w-5' />
57+
</div>
58+
)}
59+
</div>
60+
);
61+
}
Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,67 @@
11
@use 'src/views/styles/colors.module.scss';
22
@use 'src/views/styles/fonts.module.scss';
33

4-
.text {
5-
font-family: 'Roboto Flex', sans-serif;
6-
line-height: normal;
7-
font-style: normal;
8-
}
4+
@layer theme {
5+
.text {
6+
font-family: 'Roboto Flex', sans-serif;
7+
line-height: normal;
8+
font-style: normal;
9+
}
910

10-
.mini {
11-
font-size: 0.79rem;
12-
font-weight: 500;
13-
}
11+
.mini {
12+
font-size: 0.79rem;
13+
font-weight: 500;
14+
}
1415

15-
.small {
16-
font-size: 0.88875rem;
17-
font-weight: 500;
18-
}
16+
.small {
17+
font-size: 0.88875rem;
18+
font-weight: 500;
19+
}
1920

20-
.p {
21-
font-size: 1rem;
22-
font-weight: 400;
23-
letter-spacing: 0.025rem;
24-
}
21+
.p {
22+
font-size: 1rem;
23+
font-weight: 400;
24+
letter-spacing: 0.025rem;
25+
}
2526

26-
.h4 {
27-
font-size: 1.125rem;
28-
font-weight: 500;
29-
}
27+
.h4 {
28+
font-size: 1.125rem;
29+
font-weight: 500;
30+
}
3031

31-
.h3-course {
32-
font-size: 0.6875rem;
33-
font-weight: 400;
34-
line-height: 100%; /* 0.6875rem */
35-
}
32+
.h3-course {
33+
font-size: 0.6875rem;
34+
font-weight: 400;
35+
line-height: 100%; /* 0.6875rem */
36+
}
3637

37-
.h3 {
38-
font-size: 1.26563rem;
39-
font-weight: 600;
40-
text-transform: uppercase;
41-
}
38+
.h3 {
39+
font-size: 1.26563rem;
40+
font-weight: 600;
41+
text-transform: uppercase;
42+
}
4243

43-
.h2-course {
44-
font-size: 1rem;
45-
font-weight: 500;
46-
letter-spacing: 0.03125rem;
47-
text-transform: capitalize;
48-
}
44+
.h2-course {
45+
font-size: 1rem;
46+
font-weight: 500;
47+
letter-spacing: 0.03125rem;
48+
text-transform: capitalize;
49+
}
4950

50-
.h2 {
51-
font-size: 1.42375rem;
52-
font-weight: 500;
53-
}
51+
.h2 {
52+
font-size: 1.42375rem;
53+
font-weight: 500;
54+
}
5455

55-
.h1-course {
56-
font-size: 1rem;
57-
font-weight: 600;
58-
text-transform: capitalize;
59-
}
56+
.h1-course {
57+
font-size: 1rem;
58+
font-weight: 600;
59+
text-transform: capitalize;
60+
}
6061

61-
.h1 {
62-
font-size: 1.60188rem;
63-
font-weight: 700;
64-
text-transform: uppercase;
62+
.h1 {
63+
font-size: 1.60188rem;
64+
font-weight: 700;
65+
text-transform: uppercase;
66+
}
6567
}

0 commit comments

Comments
 (0)