Compare commits
13 Commits
testing-us
...
feat-confl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c75ff481 | ||
|
|
7f76af7ab3 | ||
|
|
a538f20aad | ||
|
|
0acd0b722c | ||
| ae32d0b645 | |||
|
|
f214ed7c01 | ||
|
|
e44b0c0e45 | ||
|
|
206c97c5b5 | ||
| ac71b838db | |||
|
|
4f5753917b | ||
|
|
478ab31706 | ||
|
|
42420f5502 | ||
|
|
a03bcf17b8 |
30
README.md
30
README.md
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
## Built Using
|
## Built Using
|
||||||
|
|
||||||
- React 18
|
- React 18
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- Vite 5
|
- Vite 5
|
||||||
- ESLint
|
- ESLint
|
||||||
- Prettier
|
- Prettier
|
||||||
- Semantic-Release
|
- Semantic-Release
|
||||||
- Custom Messaging & Storage Wrappers
|
- Custom Messaging & Storage Wrappers
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Clone this repo
|
1. Clone this repo
|
||||||
2. Run `pnpm install` to install and patch all the required dependencies
|
2. Run `pnpm install` to install and patch all the required dependencies
|
||||||
3. Run `pnpm run dev` to start the development server
|
|
||||||
4. Run `pnpm build` to build the extension for production
|
- If you want to run the development build:
|
||||||
|
|
||||||
|
- Run `pnpm run dev`
|
||||||
|
|
||||||
|
- If you want to build the extension for production:
|
||||||
|
|
||||||
|
- Run `pnpm build`
|
||||||
|
|
||||||
|
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
|
||||||
|
|
||||||
|
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
|
||||||
|
|
||||||
|
Navigate to the 'dist' folder and click 'select' to import the extension
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"highcharts": "^11.3.0",
|
"highcharts": "^11.3.0",
|
||||||
"highcharts-react-official": "^3.2.1",
|
"highcharts-react-official": "^3.2.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html-to-image": "^1.11.11",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-devtools-core": "^5.0.0",
|
"react-devtools-core": "^5.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -34,9 +34,9 @@ dependencies:
|
|||||||
highcharts-react-official:
|
highcharts-react-official:
|
||||||
specifier: ^3.2.1
|
specifier: ^3.2.1
|
||||||
version: 3.2.1(highcharts@11.3.0)(react@18.2.0)
|
version: 3.2.1(highcharts@11.3.0)(react@18.2.0)
|
||||||
html2canvas:
|
html-to-image:
|
||||||
specifier: ^1.4.1
|
specifier: ^1.11.11
|
||||||
version: 1.4.1
|
version: 1.11.11
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@@ -5856,11 +5856,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/base64-arraybuffer@1.0.2:
|
|
||||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/base64-js@1.5.1:
|
/base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -6553,12 +6548,6 @@ packages:
|
|||||||
postcss: 8.4.35
|
postcss: 8.4.35
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/css-line-break@2.1.0:
|
|
||||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/css-select@5.1.0:
|
/css-select@5.1.0:
|
||||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8663,12 +8652,8 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/html2canvas@1.4.1:
|
/html-to-image@1.11.11:
|
||||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==}
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
dependencies:
|
|
||||||
css-line-break: 2.1.0
|
|
||||||
text-segmentation: 1.0.3
|
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/htmlparser2@8.0.2:
|
/htmlparser2@8.0.2:
|
||||||
@@ -12430,12 +12415,6 @@ packages:
|
|||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/text-segmentation@1.0.3:
|
|
||||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/text-table@0.2.0:
|
/text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -12978,12 +12957,6 @@ packages:
|
|||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/utrie@1.0.2:
|
|
||||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
|
||||||
dependencies:
|
|
||||||
base64-arraybuffer: 1.0.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/uuid@9.0.1:
|
/uuid@9.0.1:
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|||||||
101
src/stories/components/CalendarBottomBar.stories.tsx
Normal file
101
src/stories/components/CalendarBottomBar.stories.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Course, Status } from '@shared/types/Course';
|
||||||
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { CalendarBottomBar } from '@views/components/common/CalendarBottomBar/CalendarBottomBar';
|
||||||
|
import { getCourseColors } from '../../shared/util/colors';
|
||||||
|
|
||||||
|
const exampleGovCourse: Course = new Course({
|
||||||
|
courseName: 'Nope',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'GOV',
|
||||||
|
description: ['nah', 'aint typing this', 'corndog'],
|
||||||
|
flags: ['no flag for you >:)'],
|
||||||
|
fullName: 'GOV 312L Something something',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Barrymore',
|
||||||
|
fullName: 'Bevo Barrymore',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '312L',
|
||||||
|
schedule: {
|
||||||
|
meetings: [],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.OPEN,
|
||||||
|
uniqueId: 12345,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const examplePsyCourse: Course = new Course({
|
||||||
|
courseName: 'Nope Again',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'PSY',
|
||||||
|
description: ['nah', 'aint typing this', 'corndog'],
|
||||||
|
flags: ['no flag for you >:)'],
|
||||||
|
fullName: 'PSY 317L Yada yada',
|
||||||
|
instructionMode: 'Online',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Bevo',
|
||||||
|
lastName: 'Etz',
|
||||||
|
fullName: 'Bevo Etz',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '317L',
|
||||||
|
schedule: {
|
||||||
|
meetings: [],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12346',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.CLOSED,
|
||||||
|
uniqueId: 12346,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Common/CalendarBottomBar',
|
||||||
|
component: CalendarBottomBar,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof CalendarBottomBar>;
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
courses: [
|
||||||
|
{
|
||||||
|
colors: getCourseColors('pink', 200),
|
||||||
|
courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} – ${exampleGovCourse.instructors[0].lastName}`,
|
||||||
|
status: exampleGovCourse.status,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colors: getCourseColors('slate', 500),
|
||||||
|
courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} – ${examplePsyCourse.instructors[0].lastName}`,
|
||||||
|
status: examplePsyCourse.status,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
render: props => (
|
||||||
|
<div className='outline-red outline w-292.5!'>
|
||||||
|
<CalendarBottomBar {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import CalendarHeader from '@views/components/common/CalendarHeader/CalenderHeader';
|
import CalendarHeader from '@views/components/common/CalendarHeader/CalenderHeader';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/CalendarHeader',
|
title: 'Components/Common/CalendarHeader',
|
||||||
component: CalendarHeader,
|
component: CalendarHeader,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
|
|||||||
@@ -1,5 +1,96 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
|
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
|
||||||
|
export 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/',
|
||||||
|
});
|
||||||
|
export const ExampleCourse2: Course = new Course({
|
||||||
|
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
creditHours: 3,
|
||||||
|
department: 'C S',
|
||||||
|
description: [
|
||||||
|
'Restricted to computer science majors.',
|
||||||
|
'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
|
||||||
|
'Computer Science 439 and 439H may not both be counted.',
|
||||||
|
'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
|
||||||
|
'May be counted toward the Independent Inquiry flag requirement.',
|
||||||
|
],
|
||||||
|
flags: ['Independent Inquiry'],
|
||||||
|
fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
instructors: [
|
||||||
|
new Instructor({
|
||||||
|
firstName: 'Allison',
|
||||||
|
lastName: 'Norman',
|
||||||
|
fullName: 'Allison Norman',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
isReserved: false,
|
||||||
|
number: '439',
|
||||||
|
schedule: {
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
startTime: 930,
|
||||||
|
endTime: 1050,
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Friday'],
|
||||||
|
startTime: 600,
|
||||||
|
endTime: 720,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
season: 'Spring',
|
||||||
|
year: 2024,
|
||||||
|
},
|
||||||
|
status: Status.WAITLISTED,
|
||||||
|
uniqueId: 67890,
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/ConflictsWithWarning',
|
title: 'Components/Common/ConflictsWithWarning',
|
||||||
@@ -9,8 +100,10 @@ const meta = {
|
|||||||
},
|
},
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
ConflictingCourse: { control: 'string' },
|
conflicts: { control: 'object' },
|
||||||
SectionNumber: { control: 'string' },
|
},
|
||||||
|
args: {
|
||||||
|
conflicts: [ExampleCourse, ExampleCourse2],
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof ConflictsWithWarning>;
|
} satisfies Meta<typeof ConflictsWithWarning>;
|
||||||
export default meta;
|
export default meta;
|
||||||
@@ -19,7 +112,6 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
ConflictingCourse: 'BVO 311C',
|
conflicts: [ExampleCourse, ExampleCourse2],
|
||||||
SectionNumber: '47280',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Text from '../Text/Text';
|
||||||
|
import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
|
||||||
|
import { Button } from '../Button/Button';
|
||||||
|
import ImageIcon from '~icons/material-symbols/image';
|
||||||
|
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
|
||||||
|
|
||||||
|
type CalendarBottomBarProps = {
|
||||||
|
courses: CalendarCourseCellProps[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => {
|
||||||
|
if (courses.length === -1) console.log('foo'); // dumb line to make eslint happy
|
||||||
|
return (
|
||||||
|
<div className='w-full flex py-1.25'>
|
||||||
|
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
|
||||||
|
<Text variant='h4'>Async. and Other:</Text>
|
||||||
|
<div className='h-14 inline-flex gap-2.5'>
|
||||||
|
{courses.map(course => (
|
||||||
|
<CalendarCourseBlock
|
||||||
|
courseDeptAndInstr={course.courseDeptAndInstr}
|
||||||
|
status={course.status}
|
||||||
|
colors={course.colors}
|
||||||
|
key={course.courseDeptAndInstr}
|
||||||
|
className={clsx(course.className, 'w-35!')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center pl-2.5 pr-7.5'>
|
||||||
|
<Button variant='single' color='ut-black' icon={CalendarMonthIcon}>
|
||||||
|
Save as .CAL
|
||||||
|
</Button>
|
||||||
|
<Button variant='single' color='ut-black' icon={ImageIcon}>
|
||||||
|
Save as .PNG
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ export interface CalendarCourseCellProps {
|
|||||||
timeAndLocation?: string;
|
timeAndLocation?: string;
|
||||||
status: Status;
|
status: Status;
|
||||||
colors: CourseColors;
|
colors: CourseColors;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
||||||
@@ -19,6 +20,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
|||||||
timeAndLocation,
|
timeAndLocation,
|
||||||
status,
|
status,
|
||||||
colors,
|
colors,
|
||||||
|
className,
|
||||||
}: CalendarCourseCellProps) => {
|
}: CalendarCourseCellProps) => {
|
||||||
let rightIcon: React.ReactNode | null = null;
|
let rightIcon: React.ReactNode | null = null;
|
||||||
if (status === Status.WAITLISTED) {
|
if (status === Status.WAITLISTED) {
|
||||||
@@ -34,7 +36,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-full flex justify-center rounded p-2 ${fontColor}`}
|
className={clsx('w-full flex justify-center rounded p-2', fontColor, className)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.primaryColor,
|
backgroundColor: colors.primaryColor,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, {useRef} from 'react';
|
import React, { useRef } from 'react';
|
||||||
import html2canvas from 'html2canvas';
|
import * as htmlToImage from 'html-to-image';
|
||||||
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
import { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
|
import { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
|
||||||
|
import calIcon from 'src/assets/icons/cal.svg';
|
||||||
|
import pngIcon from 'src/assets/icons/png.svg';
|
||||||
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
|
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
|
||||||
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
|
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
|
||||||
import styles from './CalendarGrid.module.scss';
|
import styles from './CalendarGrid.module.scss';
|
||||||
import calIcon from 'src/assets/icons/cal.svg';
|
|
||||||
import pngIcon from 'src/assets/icons/png.svg';
|
|
||||||
|
|
||||||
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
|
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
|
||||||
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
|
||||||
@@ -34,19 +34,40 @@ interface Props {
|
|||||||
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
||||||
* @param props
|
* @param props
|
||||||
*/
|
*/
|
||||||
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props> ): JSX.Element {
|
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
|
||||||
const calendarRef = useRef(null); // Create a ref for the calendar grid
|
const calendarRef = useRef(null); // Create a ref for the calendar grid
|
||||||
|
|
||||||
const saveAsPNG = () => {
|
const saveAsPNG = () => {
|
||||||
if (calendarRef.current) {
|
htmlToImage
|
||||||
html2canvas(calendarRef.current).then((canvas) => {
|
.toPng(calendarRef.current, {
|
||||||
// Create an a element to trigger download
|
backgroundColor: 'white',
|
||||||
const a = document.createElement('a');
|
style: {
|
||||||
a.href = canvas.toDataURL('image/png');
|
background: 'white',
|
||||||
a.download = 'calendar.png';
|
marginTop: '20px',
|
||||||
a.click();
|
marginBottom: '20px',
|
||||||
|
marginRight: '20px',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(dataUrl => {
|
||||||
|
let img = new Image();
|
||||||
|
img.src = dataUrl;
|
||||||
|
fetch(dataUrl)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const href = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = href;
|
||||||
|
link.download = 'my-schedule.png';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error downloading file:', error));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('oops, something went wrong!', error);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,19 +106,22 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
|
|||||||
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
|
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CalendarCourseCell courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
<CalendarCourseCell
|
||||||
status={block.componentProps.status} colors={block.componentProps.colors}/>
|
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
||||||
|
status={block.componentProps.status}
|
||||||
|
colors={block.componentProps.colors}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<div className={styles.divider}></div> {/* First divider */}
|
<div className={styles.divider} /> {/* First divider */}
|
||||||
<button className={styles.calendarButton}>
|
<button className={styles.calendarButton}>
|
||||||
<img src={calIcon} className={styles.buttonIcon} alt="CAL" />
|
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
|
||||||
Save as .CAL
|
Save as .CAL
|
||||||
</button>
|
</button>
|
||||||
<div className={styles.divider}></div> {/* Second divider */}
|
<div className={styles.divider} /> {/* Second divider */}
|
||||||
<button onClick={saveAsPNG} className={styles.calendarButton}>
|
<button onClick={saveAsPNG} className={styles.calendarButton}>
|
||||||
<img src={pngIcon} className={styles.buttonIcon} alt="PNG" />
|
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
|
||||||
Save as .PNG
|
Save as .PNG
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,40 +12,41 @@ import ScheduleTotalHoursAndCourses from '../ScheduleTotalHoursAndCourses/Schedu
|
|||||||
import CourseStatus from '../CourseStatus/CourseStatus';
|
import CourseStatus from '../CourseStatus/CourseStatus';
|
||||||
|
|
||||||
const CalendarHeader = () => (
|
const CalendarHeader = () => (
|
||||||
<div
|
<div className='min-h-79px min-w-672px flex px-0 py-15'>
|
||||||
style={{
|
<div className='flex flex-row gap-20'>
|
||||||
display: 'flex',
|
<div className='flex gap-10'>
|
||||||
minWidth: '672px',
|
<div className='flex gap-1'>
|
||||||
minHeight: '79px',
|
<Button variant='single' icon={MenuIcon} color='ut-gray' />
|
||||||
padding: '15px 0px',
|
<div className='flex items-center'>
|
||||||
justifyContent: 'space-between',
|
<LogoIcon style={{ marginRight: '5px' }} />
|
||||||
alignItems: 'center',
|
<div className='flex flex-col gap-1 whitespace-nowrap'>
|
||||||
alignContent: 'center',
|
<Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'>
|
||||||
rowGap: '10px',
|
UT Registration
|
||||||
alignSelf: 'stretch',
|
</Text>
|
||||||
flexWrap: 'wrap',
|
<Text className='leading-trim text-cap font-roboto text-base text-ut-orange font-medium'>
|
||||||
}}
|
Plus
|
||||||
>
|
</Text>
|
||||||
<Button variant='single' icon={MenuIcon} color='ut-gray' />
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
</div>
|
||||||
<LogoIcon style={{ marginRight: '5px' }} />
|
<div className='flex flex-col'>
|
||||||
<Text>Your Logo Text</Text>
|
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
|
||||||
|
DATA UPDATED ON: 12:00 AM 02/01/2024
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row items-center space-x-8'>
|
||||||
|
<div className='flex flex-row space-x-4'>
|
||||||
|
<CourseStatus size='small' status={Status.WAITLISTED} />
|
||||||
|
<CourseStatus size='small' status={Status.CLOSED} />
|
||||||
|
<CourseStatus size='small' status={Status.CANCELLED} />
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row'>
|
||||||
|
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
||||||
|
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
||||||
|
<Button variant='single' icon={SettingsIcon} color='ut-black' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
|
|
||||||
|
|
||||||
<CourseStatus size='small' status={Status.WAITLISTED} />
|
|
||||||
<CourseStatus size='small' status={Status.CLOSED} />
|
|
||||||
<CourseStatus size='small' status={Status.CANCELLED} />
|
|
||||||
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Button variant='outline' icon={UndoIcon} color='ut-black' />
|
|
||||||
<Button variant='outline' icon={RedoIcon} color='ut-black' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant='outline' icon={SettingsIcon} color='ut-black' />
|
|
||||||
|
|
||||||
<Divider type='solid' />
|
<Divider type='solid' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,43 +1,37 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Course } from 'src/shared/types/Course';
|
||||||
|
import clsx from 'clsx';
|
||||||
import Text from '../Text/Text';
|
import Text from '../Text/Text';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for ConflictWithWarningProps
|
* Props for ConflictWithWarningProps
|
||||||
*/
|
*/
|
||||||
export interface ConflictsWithWarningProps {
|
export interface ConflictsWithWarningProps {
|
||||||
ConflictingCourse: string;
|
className?: string;
|
||||||
SectionNumber: string;
|
conflicts: Course[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ConflictsWithWarning component is used to display a warning message when a course conflicts
|
* The ConflictsWithWarning component is used to display a warning message when a course conflicts
|
||||||
* with another course as part of the labels and details section
|
* with another course as part of the labels and details section
|
||||||
*
|
*
|
||||||
* @param props ConflictsWithWarningProps
|
* @param props ConflictsWithWarningProps
|
||||||
*/
|
*/
|
||||||
export default function ConflictsWithWarning( { ConflictingCourse, SectionNumber }: ConflictsWithWarningProps): JSX.Element {
|
export default function ConflictsWithWarning({ className, conflicts }: ConflictsWithWarningProps): JSX.Element {
|
||||||
const UniqueCourseConflictText = `${ConflictingCourse} (${SectionNumber})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5">
|
|
||||||
<ConflictsWithoutWarningText>
|
|
||||||
Conflicts With:
|
|
||||||
</ConflictsWithoutWarningText>
|
|
||||||
<ConflictsWithoutWarningText>
|
|
||||||
{UniqueCourseConflictText}
|
|
||||||
</ConflictsWithoutWarningText>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConflictsWithoutWarningText( {children}: {children: string} ) {
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
variant='mini'
|
variant='mini'
|
||||||
as='span'
|
className={clsx(
|
||||||
className='text-white'
|
className,
|
||||||
|
'min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5 text-white'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
<div>Conflicts With:</div>
|
||||||
|
{conflicts.map(course => (
|
||||||
|
<div>
|
||||||
|
{`${course.department} ${course.number} (${course.uniqueId})`}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function ScheduleTotalHoursAndCourses({
|
|||||||
totalCourses,
|
totalCourses,
|
||||||
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
|
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className='min-w-64 flex flex-wrap content-center items-baseline gap-2 uppercase'>
|
<div className='min-w-64 flex whitespace-nowrap content-center items-baseline gap-2 uppercase'>
|
||||||
<Text className='text-[#BF5700]' variant='h1' as='span'>
|
<Text className='text-[#BF5700]' variant='h1' as='span'>
|
||||||
{`${scheduleName}: `}
|
{`${scheduleName}: `}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { UserSchedule } from '@shared/types/UserSchedule';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Button } from '../../common/Button/Button';
|
import { Button } from '../../common/Button/Button';
|
||||||
import Icon from '../../common/Icon/Icon';
|
|
||||||
import Text from '../../common/Text/Text';
|
|
||||||
import styles from './TableRow.module.scss';
|
import styles from './TableRow.module.scss';
|
||||||
|
import ConflictsWithWarning from '../../common/ConflictsWithWarning/ConflictsWithWarning';
|
||||||
|
import AddIcon from '~icons/material-symbols/add-circle';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
@@ -54,7 +54,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
|||||||
return () => {
|
return () => {
|
||||||
element.classList.remove(styles.inActiveSchedule);
|
element.classList.remove(styles.inActiveSchedule);
|
||||||
};
|
};
|
||||||
}, [activeSchedule, element.classList]);
|
}, [activeSchedule, course, element.classList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSchedule || !course) {
|
if (!activeSchedule || !course) {
|
||||||
@@ -84,20 +84,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
|||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<>
|
<>
|
||||||
<Button className={styles.rowButton} onClick={onClick} variant='secondary'>
|
<Button
|
||||||
<Icon name='bar_chart' color='white' size='medium' />
|
icon={AddIcon}
|
||||||
</Button>
|
className={styles.rowButton}
|
||||||
{conflicts.length > 0 && (
|
color='ut-burntorange'
|
||||||
<div className={styles.conflictTooltip}>
|
onClick={onClick}
|
||||||
<div className={styles.body}>
|
variant='single'
|
||||||
{conflicts.map(c => (
|
/>
|
||||||
<Text /* size='small' */ key={c.uniqueId}>
|
{conflicts.length > 0 && <ConflictsWithWarning className={styles.conflictTooltip} conflicts={conflicts} />}
|
||||||
{c.department} {c.number} ({c.uniqueId})
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>,
|
</>,
|
||||||
container
|
container
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface CalendarGridPoint {
|
|||||||
* Return type of useFlattenedCourseSchedule
|
* Return type of useFlattenedCourseSchedule
|
||||||
*/
|
*/
|
||||||
export interface CalendarGridCourse {
|
export interface CalendarGridCourse {
|
||||||
calendarGridPoint?: CalendarGridPoint;
|
calendarGridPoint: CalendarGridPoint;
|
||||||
componentProps: CalendarCourseCellProps;
|
componentProps: CalendarCourseCellProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,55 +33,70 @@ export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
|
|||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
const { courses } = activeSchedule;
|
const { courses } = activeSchedule;
|
||||||
|
|
||||||
return courses.flatMap(course => {
|
return courses
|
||||||
const {
|
.flatMap(course => {
|
||||||
status,
|
const {
|
||||||
department,
|
status,
|
||||||
instructors,
|
department,
|
||||||
schedule: { meetings },
|
instructors,
|
||||||
} = course;
|
schedule: { meetings },
|
||||||
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
} = course;
|
||||||
|
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
||||||
|
|
||||||
if (meetings.length === 0) {
|
if (meetings.length === 0) {
|
||||||
// asynch, online course
|
// asynch, online course
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 0,
|
||||||
|
startIndex: 0,
|
||||||
|
endIndex: 0,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr,
|
||||||
|
status,
|
||||||
|
colors: {
|
||||||
|
// TODO: figure out colors - these are defaults
|
||||||
|
primaryColor: 'ut-gray',
|
||||||
|
secondaryColor: 'ut-gray',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// in-person
|
||||||
|
return meetings.flatMap(meeting => {
|
||||||
|
const { days, startTime, endTime, location } = meeting;
|
||||||
|
const time = meeting.getTimeString({ separator: '-', capitalize: true });
|
||||||
|
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
||||||
|
|
||||||
|
return days.map(d => ({
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: dayToNumber[d],
|
||||||
|
startIndex: convertMinutesToIndex(startTime),
|
||||||
|
endIndex: convertMinutesToIndex(endTime),
|
||||||
|
},
|
||||||
componentProps: {
|
componentProps: {
|
||||||
courseDeptAndInstr,
|
courseDeptAndInstr,
|
||||||
|
timeAndLocation,
|
||||||
status,
|
status,
|
||||||
colors: {
|
colors: {
|
||||||
// TODO: figure out colors - these are defaults
|
// TODO: figure out colors - these are defaults
|
||||||
primaryColor: 'ut-gray',
|
primaryColor: 'ut-orange',
|
||||||
secondaryColor: 'ut-gray',
|
secondaryColor: 'ut-orange',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}));
|
||||||
];
|
});
|
||||||
}
|
})
|
||||||
|
.sort((a: CalendarGridCourse, b: CalendarGridCourse) => {
|
||||||
// in-person
|
if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) {
|
||||||
return meetings.flatMap(meeting => {
|
return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex;
|
||||||
const { days, startTime, endTime, location } = meeting;
|
}
|
||||||
const time = meeting.getTimeString({ separator: '-', capitalize: true });
|
if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) {
|
||||||
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex;
|
||||||
|
}
|
||||||
return days.map(d => ({
|
return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex;
|
||||||
calendarGridPoint: {
|
|
||||||
dayIndex: dayToNumber[d],
|
|
||||||
startIndex: convertMinutesToIndex(startTime),
|
|
||||||
endIndex: convertMinutesToIndex(endTime),
|
|
||||||
},
|
|
||||||
componentProps: {
|
|
||||||
courseDeptAndInstr,
|
|
||||||
timeAndLocation,
|
|
||||||
status,
|
|
||||||
colors: {
|
|
||||||
// TODO: figure out colors - these are defaults
|
|
||||||
primaryColor: 'ut-orange',
|
|
||||||
secondaryColor: 'ut-orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user