Merge branch 'main' into ListComponentContinuation
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"airbnb-base",
|
"airbnb-base",
|
||||||
"airbnb/rules/react",
|
"airbnb/rules/react",
|
||||||
"airbnb-typescript",
|
"airbnb-typescript",
|
||||||
|
"@unocss",
|
||||||
"prettier",
|
"prettier",
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -203,6 +204,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"import/extensions": "off",
|
||||||
"no-restricted-syntax": [
|
"no-restricted-syntax": [
|
||||||
"error",
|
"error",
|
||||||
"ForInStatement",
|
"ForInStatement",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ExtensionRoot from 'src/views/components/common/ExtensionRoot/ExtensionRoot';
|
|
||||||
import type { Preview } from '@storybook/react';
|
import type { Preview } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ExtensionRoot from 'src/views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
import UnoCSS from 'unocss/vite';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
const root = resolve(__dirname, '../src');
|
const root = resolve(__dirname, '../src');
|
||||||
@@ -11,7 +13,7 @@ console.log(root);
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), UnoCSS(), Icons({ compiler: 'jsx', jsx: 'react' })],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
src: root,
|
src: root,
|
||||||
|
|||||||
21035
package-lock.json
generated
Normal file
21035
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -20,40 +20,49 @@
|
|||||||
"@types/sql.js": "^1.4.9",
|
"@types/sql.js": "^1.4.9",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"chrome-extension-toolkit": "^0.0.51",
|
"chrome-extension-toolkit": "^0.0.51",
|
||||||
"classnames": "^2.3.2",
|
"clsx": "^2.1.0",
|
||||||
"highcharts": "^11.2.0",
|
"highcharts": "^11.3.0",
|
||||||
"highcharts-react-official": "^3.2.1",
|
"highcharts-react-official": "^3.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-devtools-core": "^5.0.0",
|
"react-devtools-core": "^5.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-window": "^1.8.10",
|
"sass": "^1.70.0",
|
||||||
"sass": "^1.69.5",
|
"sql.js": "1.10.2",
|
||||||
"sql.js": "1.9.0",
|
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
"@crxjs/vite-plugin": "2.0.0-beta.21",
|
||||||
|
"@iconify-json/material-symbols": "^1.1.72",
|
||||||
"@storybook/addon-designs": "^7.0.9",
|
"@storybook/addon-designs": "^7.0.9",
|
||||||
"@storybook/addon-essentials": "^7.6.11",
|
"@storybook/addon-essentials": "^7.6.12",
|
||||||
"@storybook/addon-links": "^7.6.11",
|
"@storybook/addon-links": "^7.6.12",
|
||||||
"@storybook/blocks": "^7.6.11",
|
"@storybook/blocks": "^7.6.12",
|
||||||
"@storybook/react": "^7.6.11",
|
"@storybook/react": "^7.6.12",
|
||||||
"@storybook/react-vite": "^7.6.11",
|
"@storybook/react-vite": "^7.6.12",
|
||||||
"@storybook/test": "^7.6.11",
|
"@storybook/test": "^7.6.12",
|
||||||
"@types/chrome": "^0.0.254",
|
"@svgr/core": "^8.1.0",
|
||||||
"@types/node": "^20.10.5",
|
"@svgr/plugin-jsx": "^8.1.0",
|
||||||
|
"@types/chrome": "^0.0.260",
|
||||||
|
"@types/node": "^20.11.16",
|
||||||
"@types/prompts": "^2.4.9",
|
"@types/prompts": "^2.4.9",
|
||||||
"@types/react": "^18.2.45",
|
"@types/react": "^18.2.51",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/semver": "^7.5.6",
|
"@types/semver": "^7.5.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.15.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@unocss/eslint-config": "^0.58.4",
|
||||||
"cssnano": "^6.0.2",
|
"@unocss/postcss": "^0.58.4",
|
||||||
"cssnano-preset-advanced": "^6.0.2",
|
"@unocss/preset-uno": "^0.58.4",
|
||||||
"dotenv": "^16.0.3",
|
"@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",
|
||||||
|
"cssnano": "^6.0.3",
|
||||||
|
"cssnano-preset-advanced": "^6.0.3",
|
||||||
|
"dotenv": "^16.4.1",
|
||||||
"es-module-lexer": "^1.4.1",
|
"es-module-lexer": "^1.4.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
@@ -62,22 +71,24 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsdoc": "^46.10.1",
|
"eslint-plugin-jsdoc": "^48.0.4",
|
||||||
"eslint-plugin-prettier": "^5.1.2",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"eslint-plugin-storybook": "^0.6.15",
|
"eslint-plugin-storybook": "^0.6.15",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.33",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.2.4",
|
||||||
"react-dev-utils": "^12.0.1",
|
"react-dev-utils": "^12.0.1",
|
||||||
"react-devtools": "^4.27.1",
|
"react-devtools": "^5.0.0",
|
||||||
"storybook": "^7.6.11",
|
"storybook": "^7.6.12",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.10",
|
"unocss": "^0.58.4",
|
||||||
"vite-plugin-inspect": "^0.8.1"
|
"unplugin-icons": "^0.18.3",
|
||||||
|
"vite": "^5.0.12",
|
||||||
|
"vite-plugin-inspect": "^0.8.3"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|||||||
2427
pnpm-lock.yaml
generated
2427
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins:
|
plugins: {
|
||||||
process.env.NODE_ENV !== 'development'
|
cssnano: process.env.NODE_ENV !== 'development' ? {} : false,
|
||||||
? [
|
// '@unocss/postcss': {},
|
||||||
require('cssnano')({
|
},
|
||||||
preset: 'advanced',
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
BIN
public/fonts/roboto-flex.woff2
Normal file
BIN
public/fonts/roboto-flex.woff2
Normal file
Binary file not shown.
50
src/shared/util/colors.ts
Normal file
50
src/shared/util/colors.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
24
src/shared/util/icons.tsx
Normal file
24
src/shared/util/icons.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { SVGProps } from 'react';
|
||||||
|
import ClosedIcon from '~icons/material-symbols/lock';
|
||||||
|
import WaitlistIcon from '~icons/material-symbols/timelapse';
|
||||||
|
import CancelledIcon from '~icons/material-symbols/warning';
|
||||||
|
import { Status } from '../types/Course';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Icon component based on status
|
||||||
|
* @param props.status status
|
||||||
|
* @returns React.ReactElement - the icon component
|
||||||
|
*/
|
||||||
|
export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: Status }): React.ReactElement {
|
||||||
|
const { status, ...rest } = props;
|
||||||
|
|
||||||
|
switch (props.status) {
|
||||||
|
case Status.WAITLISTED:
|
||||||
|
return <WaitlistIcon {...rest} />;
|
||||||
|
case Status.CLOSED:
|
||||||
|
return <ClosedIcon {...rest} />;
|
||||||
|
case Status.CANCELLED:
|
||||||
|
return <CancelledIcon {...rest} />;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/stories/components/CalendarCourseCell.stories.tsx
Normal file
67
src/stories/components/CalendarCourseCell.stories.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
import CalendarCourseCell from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/CalendarCourseCell',
|
||||||
|
component: CalendarCourseCell,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
course: { control: 'object' },
|
||||||
|
meetingIdx: { control: 'number' },
|
||||||
|
color: { control: 'color' },
|
||||||
|
},
|
||||||
|
render: (args: any) => (
|
||||||
|
<div className="w-45">
|
||||||
|
<CalendarCourseCell {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
} satisfies Meta<typeof CalendarCourseCell>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
course: new Course({
|
||||||
|
uniqueId: 123,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "311C - Bevo's Default Course",
|
||||||
|
courseName: "Bevo's Default Course",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
|
||||||
|
isReserved: false,
|
||||||
|
url: '',
|
||||||
|
flags: [],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: {
|
||||||
|
building: 'UTC',
|
||||||
|
room: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
meetingIdx: 0,
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
};
|
||||||
19
src/stories/components/CalendarGrid.stories.tsx
Normal file
19
src/stories/components/CalendarGrid.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Calendar.stories.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import Calendar from '@views/components/common/CalendarGrid/CalendarGrid';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/Calendar',
|
||||||
|
component: Calendar,
|
||||||
|
parameters: {
|
||||||
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
layout: 'centered',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
}
|
||||||
|
} satisfies Meta<typeof Calendar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
118
src/stories/components/PopupCourseBlock.stories.tsx
Normal file
118
src/stories/components/PopupCourseBlock.stories.tsx
Normal file
@@ -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<typeof PopupCourseBlock>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// 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 => (
|
||||||
|
<div className='grid grid-cols-2 max-w-2xl w-90vw gap-x-4 gap-y-2'>
|
||||||
|
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.OPEN })} />
|
||||||
|
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CLOSED })} />
|
||||||
|
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.WAITLISTED })} />
|
||||||
|
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CANCELLED })} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 => (
|
||||||
|
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
|
||||||
|
{colors.map((color, i) => (
|
||||||
|
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1046-6714&mode=design&t=5Bjr7qGHNXmjfMTc-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
57
src/stories/components/Text.stories.tsx
Normal file
57
src/stories/components/Text.stories.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Button } from 'src/views/components/common/Button/Button';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import React from 'react';
|
||||||
|
import Text from '../../views/components/common/Text/Text';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/Text',
|
||||||
|
component: Text,
|
||||||
|
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: {
|
||||||
|
children: 'The quick brown fox jumps over the lazy dog.',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
children: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Text>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'The quick brown fox jumps over the lazy dog.',
|
||||||
|
},
|
||||||
|
render: props => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<Text {...props} variant='mini' />
|
||||||
|
<Text {...props} variant='small' />
|
||||||
|
<Text {...props} variant='p' />
|
||||||
|
<Text {...props} variant='h4' />
|
||||||
|
<Text {...props} variant='h3-course' />
|
||||||
|
<Text {...props} variant='h3' />
|
||||||
|
<Text {...props} variant='h2-course' />
|
||||||
|
<Text {...props} variant='h2' />
|
||||||
|
<Text {...props} variant='h1-course' />
|
||||||
|
<Text {...props} variant='h1' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: 'figma',
|
||||||
|
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=324-389&mode=design&t=BoS5xBrpSsjgQXqv-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"chrome",
|
"chrome",
|
||||||
"node"
|
"node",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './Button.module.scss';
|
import styles from './Button.module.scss';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export function Button({
|
|||||||
<button
|
<button
|
||||||
style={style}
|
style={style}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
className={classNames(styles.button, className, styles[type ?? 'primary'], {
|
className={clsx(styles.button, className, styles[type ?? 'primary'], {
|
||||||
[styles.disabled]: disabled,
|
[styles.disabled]: disabled,
|
||||||
})}
|
})}
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
|
import ClosedIcon from '~icons/material-symbols/lock';
|
||||||
|
import WaitlistIcon from '~icons/material-symbols/timelapse';
|
||||||
|
import CancelledIcon from '~icons/material-symbols/warning';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
|
export interface CalendarCourseBlockProps {
|
||||||
|
/** The Course that the meeting is for. */
|
||||||
|
course: Course;
|
||||||
|
/* index into course meeting array to display */
|
||||||
|
meetingIdx?: number;
|
||||||
|
/** The background color for the course. */
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarCourseBlock: React.FC<CalendarCourseBlockProps> = ({ course, meetingIdx }: CalendarCourseBlockProps) => {
|
||||||
|
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
||||||
|
let rightIcon: React.ReactNode | null = null;
|
||||||
|
if (course.status === Status.WAITLISTED) {
|
||||||
|
rightIcon = <WaitlistIcon className='h-5 w-5' />;
|
||||||
|
} else if (course.status === Status.CLOSED) {
|
||||||
|
rightIcon = <ClosedIcon className='h-5 w-5' />;
|
||||||
|
} else if (course.status === Status.CANCELLED) {
|
||||||
|
rightIcon = <CancelledIcon className='h-5 w-5' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex justify-center rounded bg-slate-300 p-2 text-ut-black'>
|
||||||
|
<div className='flex flex-1 flex-col gap-1'>
|
||||||
|
<Text variant='h1-course' className='leading-[75%]!'>
|
||||||
|
{course.department} {course.number} - {course.instructors[0].lastName}
|
||||||
|
</Text>
|
||||||
|
<Text variant='h3-course' className='leading-[75%]!'>
|
||||||
|
{`${meeting.getTimeString({ separator: '–', capitalize: true })}${
|
||||||
|
meeting.location ? ` – ${meeting.location.building}` : ''
|
||||||
|
}`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{rightIcon && (
|
||||||
|
<div className='h-fit flex items-center justify-center justify-self-start rounded bg-slate-700 p-0.5 text-white'>
|
||||||
|
{rightIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarCourseBlock;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
@use 'sass:color';
|
||||||
|
@use 'src/views/styles/colors.module.scss';
|
||||||
|
|
||||||
|
.dayLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 13px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 13px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
grid-template-rows: repeat(13, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendarRow {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar {
|
||||||
|
// display: grid;
|
||||||
|
// grid-template-columns: auto repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
gap: 5px;
|
||||||
|
color: colors.$burnt_orange;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14.22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeAndGrid {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeColumn {
|
||||||
|
display: flex;
|
||||||
|
min-height: 573px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1 0 0;
|
||||||
|
border-radius: var(--border-radius-none, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeBlock {
|
||||||
|
display: flex;
|
||||||
|
width: 50px;
|
||||||
|
height: 40.279px;
|
||||||
|
min-width: 50px;
|
||||||
|
max-width: 50px;
|
||||||
|
min-height: 40px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.timeLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
max-height: 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 17px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: var(--border-radius-none, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #1A2024;
|
||||||
|
text-align: left;
|
||||||
|
height: 6.6px;
|
||||||
|
align-self: stretch;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
/* Type scale/small */
|
||||||
|
font-family: "Roboto Flex";
|
||||||
|
font-size: 14.22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
52
src/views/components/common/CalendarGrid/CalendarGrid.tsx
Normal file
52
src/views/components/common/CalendarGrid/CalendarGrid.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './CalendarGrid.module.scss';
|
||||||
|
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
|
||||||
|
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
|
||||||
|
const daysOfWeek = Object.values(DAY_MAP).filter(d => d != "Saturday" && d != "Sunday")
|
||||||
|
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
||||||
|
const grid = Array.from({ length: 5 }, () =>
|
||||||
|
Array.from({ length: 13 }, (_, columnIndex) => (
|
||||||
|
<CalendarCell key={columnIndex} />
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
const Calendar: React.FC = (props) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.calendar}>
|
||||||
|
<div className={styles.dayLabelContainer}>
|
||||||
|
{/* Empty cell in the top-left corner */}
|
||||||
|
<div className={styles.day} />
|
||||||
|
{/* Displaying day labels */}
|
||||||
|
{daysOfWeek.map(day => (
|
||||||
|
<div key={day} className={styles.day}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Displaying the rest of the calendar */}
|
||||||
|
<div className={styles.timeAndGrid}>
|
||||||
|
<div className={styles.timeColumn}>
|
||||||
|
{hoursOfDay.map((hour) => (
|
||||||
|
<div key={hour} className={styles.timeBlock}>
|
||||||
|
<div className={styles.timeLabelContainer}>
|
||||||
|
<p>{hour % 12 === 0 ? 12 : hour % 12}:00 {hour < 12 ? 'AM' : 'PM'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.calendarGrid}>
|
||||||
|
{grid.map((row, rowIndex) => (
|
||||||
|
row
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.calendarCell {
|
||||||
|
display: flex;
|
||||||
|
width: 165px;
|
||||||
|
height: 52.231px;
|
||||||
|
min-width: 45px;
|
||||||
|
min-height: 40px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 1px solid #DADCE0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hourLine {
|
||||||
|
width: 165px;
|
||||||
|
height: 1px;
|
||||||
|
border-radius: var(--border-radius-none, 0px);
|
||||||
|
background: rgba(218, 220, 224, 0.25);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './CalendarGridCell.module.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing each 1 hour time block of a calendar
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
const CalendarCell: React.FC = (props) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.calendarCell}>
|
||||||
|
<div className={styles.hourLine}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarCell;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import styles from './Card.module.scss';
|
import styles from './Card.module.scss';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -17,7 +17,7 @@ export default function Card(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={classNames(styles.card, props.className)}
|
className={clsx(styles.card, props.className)}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
data-testid={props.testId}
|
data-testid={props.testId}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import classnames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Color } from '@views/styles/colors.module.scss';
|
import { Color } from '@views/styles/colors.module.scss';
|
||||||
import styles from './Divider.module.scss';
|
import styles from './Divider.module.scss';
|
||||||
@@ -21,5 +21,5 @@ export default function Divider(props: Props) {
|
|||||||
borderStyle: props.type,
|
borderStyle: props.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <hr data-testid={props.testId} style={style} className={classnames(styles.divider, props.className)} />;
|
return <hr data-testid={props.testId} style={style} className={clsx(styles.divider, props.className)} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './ExtensionRoot.module.scss';
|
import styles from './ExtensionRoot.module.scss';
|
||||||
|
|
||||||
|
import '@unocss/reset/tailwind-compat.css';
|
||||||
|
import 'uno.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import colors, { Color } from '@views/styles/colors.module.scss';
|
import colors, { Color } from '@views/styles/colors.module.scss';
|
||||||
import fonts, { Size } from '@views/styles/fonts.module.scss';
|
import fonts, { Size } from '@views/styles/fonts.module.scss';
|
||||||
@@ -36,7 +36,7 @@ export default function Icon(props: Props) {
|
|||||||
<span
|
<span
|
||||||
data-testid={props.testId}
|
data-testid={props.testId}
|
||||||
style={style}
|
style={style}
|
||||||
className={classNames(styles.icon, props.className)}
|
className={clsx(styles.icon, props.className)}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
{props.name}
|
{props.name}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { PropsWithChildren } from 'react';
|
import React, { PropsWithChildren } from 'react';
|
||||||
import Text, { TextProps } from '../Text/Text';
|
import Text, { TextProps } from '../Text/Text';
|
||||||
import styles from './Link.module.scss';
|
import styles from './Link.module.scss';
|
||||||
@@ -29,7 +29,7 @@ export default function Link(props: PropsWithChildren<Props>) {
|
|||||||
color='bluebonnet'
|
color='bluebonnet'
|
||||||
{...passedProps}
|
{...passedProps}
|
||||||
span
|
span
|
||||||
className={classNames(
|
className={clsx(
|
||||||
styles.link,
|
styles.link,
|
||||||
{
|
{
|
||||||
[styles.disabled]: isDisabled,
|
[styles.disabled]: isDisabled,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { PropsWithChildren, useCallback } from 'react';
|
import React, { PropsWithChildren, useCallback } from 'react';
|
||||||
import styles from './Popup.module.scss';
|
import styles from './Popup.module.scss';
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ export default function Popup({ onClose, children, className, style, testId, ove
|
|||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={classNames(styles.container, {
|
className={clsx(styles.container, {
|
||||||
[styles.overlay]: overlay,
|
[styles.overlay]: overlay,
|
||||||
})}
|
})}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
<div ref={bodyRef} className={classNames(styles.body, className)}>
|
<div ref={bodyRef} className={clsx(styles.body, className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.primaryColor,
|
||||||
|
}}
|
||||||
|
className={clsx('h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3', className)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondaryColor,
|
||||||
|
}}
|
||||||
|
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
||||||
|
>
|
||||||
|
<DragIndicatorIcon className='h-6 w-6 text-white' />
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
className={clsx('flex-1 py-3.5 text-ellipsis whitespace-nowrap overflow-hidden', fontColor)}
|
||||||
|
variant='h1-course'
|
||||||
|
>
|
||||||
|
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} –{' '}
|
||||||
|
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
|
||||||
|
</Text>
|
||||||
|
{course.status !== Status.OPEN && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondaryColor,
|
||||||
|
}}
|
||||||
|
className='ml-1 flex items-center justify-center justify-self-end rounded p-1px text-white'
|
||||||
|
>
|
||||||
|
<StatusIcon status={course.status} className='h-5 w-5' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './Spinner.module.scss';
|
import styles from './Spinner.module.scss';
|
||||||
|
|
||||||
@@ -12,5 +12,5 @@ type Props = {
|
|||||||
* A simple spinner component that can be used to indicate loading.
|
* A simple spinner component that can be used to indicate loading.
|
||||||
*/
|
*/
|
||||||
export default function Spinner({ className, testId, style }: Props) {
|
export default function Spinner({ className, testId, style }: Props) {
|
||||||
return <div data-testid={testId} style={style} className={classNames(styles.spinner, className)} />;
|
return <div data-testid={testId} style={style} className={clsx(styles.spinner, className)} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,67 @@
|
|||||||
@use 'src/views/styles/colors.module.scss';
|
@use 'src/views/styles/colors.module.scss';
|
||||||
@use 'src/views/styles/fonts.module.scss';
|
@use 'src/views/styles/fonts.module.scss';
|
||||||
|
|
||||||
.text {
|
@layer theme {
|
||||||
font-family: 'Inter', sans-serif;
|
.text {
|
||||||
color: colors.$charcoal;
|
font-family: 'Roboto Flex', sans-serif;
|
||||||
line-height: initial;
|
line-height: normal;
|
||||||
}
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.light_weight {
|
.mini {
|
||||||
font-weight: fonts.$light_weight;
|
font-size: 0.79rem;
|
||||||
}
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.regular_weight {
|
.small {
|
||||||
font-weight: fonts.$regular_weight;
|
font-size: 0.88875rem;
|
||||||
}
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.normal_weight {
|
.p {
|
||||||
font-weight: fonts.$normal_weight;
|
font-size: 1rem;
|
||||||
}
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.025rem;
|
||||||
|
}
|
||||||
|
|
||||||
.semi_bold_weight {
|
.h4 {
|
||||||
font-weight: fonts.$semi_bold_weight;
|
font-size: 1.125rem;
|
||||||
}
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.bold_weight {
|
.h3-course {
|
||||||
font-weight: fonts.$bold_weight;
|
font-size: 0.6875rem;
|
||||||
}
|
font-weight: 400;
|
||||||
|
line-height: 100%; /* 0.6875rem */
|
||||||
|
}
|
||||||
|
|
||||||
.black_weight {
|
.h3 {
|
||||||
font-weight: fonts.$black_weight;
|
font-size: 1.26563rem;
|
||||||
}
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.x_small_size {
|
.h2-course {
|
||||||
font-size: fonts.$x_small_size;
|
font-size: 1rem;
|
||||||
}
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.03125rem;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
.xx_small_size {
|
.h2 {
|
||||||
font-size: fonts.$xx_small_size;
|
font-size: 1.42375rem;
|
||||||
}
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.small_size {
|
.h1-course {
|
||||||
font-size: fonts.$small_size;
|
font-size: 1rem;
|
||||||
}
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
.medium_size {
|
.h1 {
|
||||||
font-size: fonts.$medium_size;
|
font-size: 1.60188rem;
|
||||||
}
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
.large_size {
|
}
|
||||||
font-size: fonts.$large_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.x_large_size {
|
|
||||||
font-size: fonts.$x_large_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xx_large_size {
|
|
||||||
font-size: fonts.$xx_large_size;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,32 @@
|
|||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { PropsWithChildren } from 'react';
|
import React, { PropsWithChildren } from 'react';
|
||||||
import colors, { Color } from '@views/styles/colors.module.scss';
|
|
||||||
import { Size, Weight } from '@views/styles/fonts.module.scss';
|
|
||||||
import styles from './Text.module.scss';
|
import styles from './Text.module.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type TextProps = {
|
export type TextProps = {
|
||||||
color?: Color;
|
variant?: Variant;
|
||||||
weight?: Weight;
|
} & (
|
||||||
size?: Size;
|
| (React.HTMLAttributes<HTMLSpanElement> & { as?: 'span' })
|
||||||
span?: boolean;
|
| (React.HTMLAttributes<HTMLDivElement> & { as: 'div' })
|
||||||
className?: string;
|
);
|
||||||
onClick?: () => void;
|
|
||||||
title?: string;
|
const variants = ['mini', 'small', 'p', 'h4', 'h3-course', 'h3', 'h2-course', 'h2', 'h1-course', 'h1'] as const;
|
||||||
align?: React.CSSProperties['textAlign'];
|
|
||||||
style?: React.CSSProperties;
|
type Variant = (typeof variants)[number];
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable Text component with props that build on top of the design system for the extension
|
* A reusable Text component with props that build on top of the design system for the extension
|
||||||
*/
|
*/
|
||||||
export default function Text(props: PropsWithChildren<TextProps>) {
|
export default function Text({ variant, as, className, ...props }: PropsWithChildren<TextProps>) {
|
||||||
const style: React.CSSProperties = {
|
const mergedClassName = clsx(styles.text, styles[variant], className);
|
||||||
...props.style,
|
|
||||||
textAlign: props.align,
|
|
||||||
color: props.color ? colors[props.color] : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const weightClass = `${props.weight ?? 'regular'}_weight`;
|
if (as === 'div') return <div className={mergedClassName} {...props} />;
|
||||||
const fontSizeClass = `${props.size ?? 'medium'}_size`;
|
|
||||||
|
|
||||||
const className = classNames(styles.text, styles[weightClass], styles[fontSizeClass], props.className);
|
|
||||||
|
|
||||||
if (props.span) {
|
|
||||||
return (
|
return (
|
||||||
<span title={props.title} className={className} style={style} onClick={props.onClick}>
|
<span className={mergedClassName} {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div title={props.title} className={className} style={style} onClick={props.onClick}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Course } from '@shared/types/Course';
|
import { Course } from '@shared/types/Course';
|
||||||
import classNames from 'classnames';
|
import clsx from 'clsx';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Spinner from '@views/components/common/Spinner/Spinner';
|
import Spinner from '@views/components/common/Spinner/Spinner';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
@@ -64,7 +64,7 @@ interface LineProps {
|
|||||||
function DescriptionLine({ line }: LineProps) {
|
function DescriptionLine({ line }: LineProps) {
|
||||||
const lowerCaseLine = line.toLowerCase();
|
const lowerCaseLine = line.toLowerCase();
|
||||||
|
|
||||||
const className = classNames({
|
const className = clsx({
|
||||||
[styles.prerequisite]: lowerCaseLine.includes('prerequisite'),
|
[styles.prerequisite]: lowerCaseLine.includes('prerequisite'),
|
||||||
[styles.onlyOne]:
|
[styles.onlyOne]:
|
||||||
lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'),
|
lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'),
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
|
@each $weight in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
src: url('@public/fonts/inter-#{$weights}.woff2') format('woff2');
|
src: url('@public/fonts/inter-#{$weight}.woff2') format('woff2');
|
||||||
font-display: auto;
|
font-display: auto;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: #{$weights};
|
font-weight: #{$weight};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Flex';
|
||||||
|
src: url('@public/fonts/roboto-flex.woff2') format('woff2');
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons Round';
|
font-family: 'Material Icons Round';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -16,34 +23,12 @@
|
|||||||
src: url('@public/fonts/material-icons.woff2') format('woff2');
|
src: url('@public/fonts/material-icons.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
$light_weight: 300;
|
$normal_weight: 500; // Used by <Icon>, will be removed later
|
||||||
$regular_weight: 400;
|
|
||||||
$normal_weight: 500;
|
|
||||||
$semi_bold_weight: 600;
|
|
||||||
$bold_weight: 700;
|
|
||||||
$black_weight: 900;
|
|
||||||
|
|
||||||
$xx_small_size: 4px;
|
|
||||||
$x_small_size: 8px;
|
|
||||||
$small_size: 12px;
|
|
||||||
$medium_size: 16px;
|
$medium_size: 16px;
|
||||||
$large_size: 20px;
|
|
||||||
$x_large_size: 32px;
|
|
||||||
$xx_large_size: 48px;
|
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
light_weight: $light_weight;
|
|
||||||
regular_weight: $regular_weight;
|
|
||||||
normal_weight: $normal_weight;
|
normal_weight: $normal_weight;
|
||||||
semi_bold_weight: $semi_bold_weight;
|
|
||||||
bold_weight: $bold_weight;
|
|
||||||
black_weight: $black_weight;
|
|
||||||
|
|
||||||
xx_small_size: $xx_small_size;
|
|
||||||
x_small_size: $x_small_size;
|
|
||||||
small_size: $small_size;
|
|
||||||
medium_size: $medium_size;
|
medium_size: $medium_size;
|
||||||
large_size: $large_size;
|
|
||||||
x_large_size: $x_large_size;
|
|
||||||
xx_large_size: $xx_large_size;
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/views/styles/fonts.module.scss.d.ts
vendored
11
src/views/styles/fonts.module.scss.d.ts
vendored
@@ -2,25 +2,14 @@
|
|||||||
* the type for all the weight scss variables exported from fonts.module.scss
|
* the type for all the weight scss variables exported from fonts.module.scss
|
||||||
*/
|
*/
|
||||||
export interface IWeights {
|
export interface IWeights {
|
||||||
light_weight: number;
|
|
||||||
regular_weight: number;
|
|
||||||
normal_weight: number;
|
normal_weight: number;
|
||||||
bold_weight: number;
|
|
||||||
semi_bold_weight: number;
|
|
||||||
black_weight: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the type for all the size scss variables exported from fonts.module.scss
|
* the type for all the size scss variables exported from fonts.module.scss
|
||||||
*/
|
*/
|
||||||
export interface ISizes {
|
export interface ISizes {
|
||||||
xx_small_size: number;
|
|
||||||
x_small_size: number;
|
|
||||||
small_size: number;
|
|
||||||
medium_size: number;
|
medium_size: number;
|
||||||
large_size: number;
|
|
||||||
x_large_size: number;
|
|
||||||
xx_large_size: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A utility type that removes the _weight postfix from the variable names for weights */
|
/** A utility type that removes the _weight postfix from the variable names for weights */
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="unplugin-icons/types/react" />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"types": [
|
"types": [
|
||||||
"vite/client",
|
"vite/client",
|
||||||
|
"unplugin-icons/types/react",
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"package.json",
|
"package.json",
|
||||||
".eslintrc",
|
".eslintrc",
|
||||||
"postcss.config.cjs",
|
"postcss.config.cjs",
|
||||||
".storybook"
|
".storybook",
|
||||||
|
"unocss.config.ts",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
58
unocss.config.ts
Normal file
58
unocss.config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import presetUno from '@unocss/preset-uno';
|
||||||
|
import presetWebFonts from '@unocss/preset-web-fonts';
|
||||||
|
import transformerDirectives from '@unocss/transformer-directives';
|
||||||
|
import transformerVariantGroup from '@unocss/transformer-variant-group';
|
||||||
|
import { defineConfig } from 'unocss';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
rules: [
|
||||||
|
['btn-transition', { transition: 'color 180ms, border-color 150ms, background-color 150ms, transform 50ms' }],
|
||||||
|
[
|
||||||
|
'ring-offset-0',
|
||||||
|
{
|
||||||
|
'--un-ring-offset-width': '0px',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
shortcuts: {
|
||||||
|
focusable: 'outline-none ring-blue-500/50 dark:ring-blue-400/60 ring-0 focus-visible:ring-4',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
easing: {
|
||||||
|
'in-out-expo': 'cubic-bezier(.46, 0, .21, 1)',
|
||||||
|
'out-expo': 'cubic-bezier(0.19, 1, 0.22, 1)',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
ut: {
|
||||||
|
'burnt-orange': '#BF5700',
|
||||||
|
black: '#333F48',
|
||||||
|
orange: '#f8971f',
|
||||||
|
yellow: '#ffd600',
|
||||||
|
'light-green': '#a6cd57',
|
||||||
|
green: '#579d42',
|
||||||
|
teal: '#00a9b7',
|
||||||
|
blue: '#005f86',
|
||||||
|
gray: '#9cadb7',
|
||||||
|
'off-white': '#d6d2c4',
|
||||||
|
concrete: '#95a5a6',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
red: '#af2e2d',
|
||||||
|
black: '#1a2024',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
presetUno(),
|
||||||
|
presetWebFonts({
|
||||||
|
provider: 'none',
|
||||||
|
fonts: {
|
||||||
|
sans: {
|
||||||
|
name: 'Roboto Flex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
transformers: [transformerVariantGroup(), transformerDirectives()],
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { crx } from '@crxjs/vite-plugin';
|
import { crx } from '@crxjs/vite-plugin';
|
||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
import UnoCSS from 'unocss/vite';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
import { Plugin, ResolvedConfig, ViteDevServer, defineConfig } from 'vite';
|
import { Plugin, ResolvedConfig, ViteDevServer, defineConfig } from 'vite';
|
||||||
import inspect from 'vite-plugin-inspect';
|
import inspect from 'vite-plugin-inspect';
|
||||||
import manifest from './src/manifest';
|
import manifest from './src/manifest';
|
||||||
@@ -44,7 +46,8 @@ let server: ViteDevServer;
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
// crx({ manifest, contentScripts: { preambleCode } }),
|
UnoCSS(),
|
||||||
|
Icons({ compiler: 'jsx', jsx: 'react' }),
|
||||||
crx({ manifest }),
|
crx({ manifest }),
|
||||||
inspect(),
|
inspect(),
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user