Compare commits

..

1 Commits

Author SHA1 Message Date
Vinson Zheng
0eefaa1860 Halfway Tailwind conversion 2024-02-17 19:37:45 -06:00
8 changed files with 100 additions and 192 deletions

View File

@@ -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",
"html-to-image": "^1.11.11", "html2canvas": "^1.4.1",
"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
View File

@@ -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)
html-to-image: html2canvas:
specifier: ^1.11.11 specifier: ^1.4.1
version: 1.11.11 version: 1.4.1
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@@ -5856,6 +5856,11 @@ 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
@@ -6548,6 +6553,12 @@ 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:
@@ -8652,8 +8663,12 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/html-to-image@1.11.11: /html2canvas@1.4.1:
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
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:
@@ -12415,6 +12430,12 @@ 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
@@ -12957,6 +12978,12 @@ 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

View File

@@ -1,96 +1,5 @@
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',
@@ -100,10 +9,8 @@ const meta = {
}, },
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
conflicts: { control: 'object' }, ConflictingCourse: { control: 'string' },
}, SectionNumber: { control: 'string' },
args: {
conflicts: [ExampleCourse, ExampleCourse2],
}, },
} satisfies Meta<typeof ConflictsWithWarning>; } satisfies Meta<typeof ConflictsWithWarning>;
export default meta; export default meta;
@@ -112,6 +19,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
conflicts: [ExampleCourse, ExampleCourse2], ConflictingCourse: 'BVO 311C',
SectionNumber: '47280',
}, },
}; };

View File

@@ -109,7 +109,7 @@
background-color: transparent; background-color: transparent;
color: #333; color: #333;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }
@@ -124,4 +124,4 @@
height: 30px; height: 30px;
width: 1px; width: 1px;
background-color: grey; background-color: grey;
} }

View File

@@ -1,5 +1,5 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import * as htmlToImage from 'html-to-image'; import html2canvas from 'html2canvas';
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 calIcon from 'src/assets/icons/cal.svg';
@@ -15,9 +15,11 @@ for (let i = 0; i < 13; i++) {
const row = []; const row = [];
let hour = hoursOfDay[i]; let hour = hoursOfDay[i];
row.push( row.push(
<div key={hour} className={styles.timeBlock}> <div key={hour} className='flex flex-col items-end'>
<div className={styles.timeLabelContainer}> <div className='flex flex-1 flex-col items-end gap-17'>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p> <p className='font-roboto-flex mb-0 mr-10 mt-[-10px] h-6.6 self-stretch text-left text-gray-900 font-normal'>
{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}
</p>
</div> </div>
</div> </div>
); );
@@ -38,53 +40,21 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
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 = () => {
htmlToImage if (calendarRef.current) {
.toPng(calendarRef.current, { html2canvas(calendarRef.current).then(canvas => {
backgroundColor: 'white', // Create an a element to trigger download
style: { const a = document.createElement('a');
background: 'white', a.href = canvas.toDataURL('image/png');
marginTop: '20px', a.download = 'calendar.png';
marginBottom: '20px', a.click();
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 (
<div className={styles.calendar}> <div className='relative flex flex-col gap-10'>
<div className={styles.dayLabelContainer} /> <div className='h-13 min-h-13 min-w-40 flex flex-1 flex-row items-center justify-center gap-10 pb-15' />
{/* Displaying the rest of the calendar */} <div ref={calendarRef} className='flex'>
<div ref={calendarRef} className={styles.timeAndGrid}>
{/* <div className={styles.timeColumn}>
<div className={styles.timeBlock}></div>
{hoursOfDay.map((hour) => (
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{hour % 12 === 0 ? 12 : hour % 12} {hour < 12 ? 'AM' : 'PM'}</p>
</div>
</div>
))}
</div> */}
<div className={styles.calendarGrid}> <div className={styles.calendarGrid}>
{/* Displaying day labels */} {/* Displaying day labels */}
<div className={styles.timeBlock} /> <div className={styles.timeBlock} />

View File

@@ -1,37 +1,43 @@
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 {
className?: string; ConflictingCourse: string;
conflicts: Course[]; SectionNumber: string;
} }
/** /**
* 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({ className, conflicts }: ConflictsWithWarningProps): JSX.Element { export default function ConflictsWithWarning( { ConflictingCourse, SectionNumber }: 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'
className={clsx( as='span'
className, className='text-white'
'min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5 text-white'
)}
> >
<div>Conflicts With:</div> {children}
{conflicts.map(course => (
<div>
{`${course.department} ${course.number} (${course.uniqueId})`}
</div>
))}
</Text> </Text>
); );
} }

View File

@@ -87,9 +87,7 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
className={styles.button} className={styles.button}
title='Search for this professor on RateMyProfessor' title='Search for this professor on RateMyProfessor'
> >
<Text /* size='medium' weight='regular' */color='white'> <Text /* size='medium' weight='regular' */ color='white'>RateMyProf</Text>
RateMyProf
</Text>
<Icon className={styles.icon} color='white' name='school' size='medium' /> <Icon className={styles.icon} color='white' name='school' size='medium' />
</Button> </Button>
<Button <Button
@@ -98,9 +96,7 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
className={styles.button} className={styles.button}
title='Search for syllabi for this course' title='Search for syllabi for this course'
> >
<Text /* size='medium' weight='regular' */ color='white'> <Text /* size='medium' weight='regular' */ color='white'>Syllabi</Text>
Syllabi
</Text>
<Icon className={styles.icon} color='white' name='grading' size='medium' /> <Icon className={styles.icon} color='white' name='grading' size='medium' />
</Button> </Button>
<Button <Button
@@ -109,9 +105,7 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
className={styles.button} className={styles.button}
title='Search for textbooks for this course' title='Search for textbooks for this course'
> >
<Text /* size='medium' weight='regular' color='white' */> <Text /* size='medium' weight='regular' color='white' */>Textbook</Text>
Textbook
</Text>
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' /> <Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
</Button> </Button>
<Button <Button
@@ -121,10 +115,7 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
variant={isCourseSaved ? 'danger' : 'success'} variant={isCourseSaved ? 'danger' : 'success'}
className={styles.button} className={styles.button}
> >
<Text /* size='medium' weight='regular' color='white' */>{isCourseSaved ? 'Remove' : 'Add'}</Text>
<Text /* size='medium' weight='regular' color='white' */ >
{isCourseSaved ? 'Remove' : 'Add'}
</Text>
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' /> <Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
</Button> </Button>
</Card> </Card>

View File

@@ -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, course, element.classList]); }, [activeSchedule, element.classList]);
useEffect(() => { useEffect(() => {
if (!activeSchedule || !course) { if (!activeSchedule || !course) {
@@ -84,14 +84,20 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
return ReactDOM.createPortal( return ReactDOM.createPortal(
<> <>
<Button <Button className={styles.rowButton} onClick={onClick} variant='secondary'>
icon={AddIcon} <Icon name='bar_chart' color='white' size='medium' />
className={styles.rowButton} </Button>
color='ut-burntorange' {conflicts.length > 0 && (
onClick={onClick} <div className={styles.conflictTooltip}>
variant='single' <div className={styles.body}>
/> {conflicts.map(c => (
{conflicts.length > 0 && <ConflictsWithWarning className={styles.conflictTooltip} conflicts={conflicts} />} <Text /* size='small' */ key={c.uniqueId}>
{c.department} {c.number} ({c.uniqueId})
</Text>
))}
</div>
</div>
)}
</>, </>,
container container
); );