Merge branch 'hackathon' into Som

This commit is contained in:
knownotunknown
2024-02-17 17:07:06 -06:00
11 changed files with 313 additions and 97 deletions

View File

@@ -24,6 +24,7 @@
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
"html2canvas": "^1.4.1",
"react": "^18.2.0",
"react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0",

34
pnpm-lock.yaml generated
View File

@@ -34,6 +34,9 @@ dependencies:
highcharts-react-official:
specifier: ^3.2.1
version: 3.2.1(highcharts@11.3.0)(react@18.2.0)
html2canvas:
specifier: ^1.4.1
version: 1.4.1
react:
specifier: ^18.2.0
version: 18.2.0
@@ -5853,6 +5856,11 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
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:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
@@ -6545,6 +6553,12 @@ packages:
postcss: 8.4.35
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:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
@@ -8649,6 +8663,14 @@ packages:
engines: {node: '>=8'}
dev: true
/html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
dev: false
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
@@ -12408,6 +12430,12 @@ packages:
minimatch: 3.1.2
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:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
@@ -12950,6 +12978,12 @@ packages:
engines: {node: '>= 0.4.0'}
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:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true

View 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>
),
};

View File

@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import CalendarHeader from '@views/components/common/CalendarHeader/CalenderHeader';
const meta = {
title: 'Components/CalendarHeader',
title: 'Components/Common/CalendarHeader',
component: CalendarHeader,
parameters: {
layout: 'centered',

View File

@@ -17,7 +17,7 @@ export default function ImportantLinks({ className }: Props) {
<Text variant='h3'>Important Links</Text>
<a
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/'
className='text-ut-burntorange flex items-center gap-0.5'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
@@ -26,7 +26,7 @@ export default function ImportantLinks({ className }: Props) {
</a>
<a
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20236/'
className='text-ut-burntorange flex items-center gap-0.5'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
@@ -35,7 +35,7 @@ export default function ImportantLinks({ className }: Props) {
</a>
<a
href='https://utdirect.utexas.edu/registrar/ris.WBX'
className='text-ut-burntorange flex items-center gap-0.5'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
@@ -44,7 +44,7 @@ export default function ImportantLinks({ className }: Props) {
</a>
<a
href='https://utdirect.utexas.edu/registration/chooseSemester.WBX'
className='text-ut-burntorange flex items-center gap-0.5'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
@@ -53,7 +53,7 @@ export default function ImportantLinks({ className }: Props) {
</a>
<a
href='https://utdirect.utexas.edu/apps/degree/audits/'
className='text-ut-burntorange flex items-center gap-0.5'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>

View File

@@ -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>
);
};

View File

@@ -12,6 +12,7 @@ export interface CalendarCourseCellProps {
timeAndLocation?: string;
status: Status;
colors: CourseColors;
className?: string;
}
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
@@ -19,6 +20,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
timeAndLocation,
status,
colors,
className,
}: CalendarCourseCellProps) => {
let rightIcon: React.ReactNode | null = null;
if (status === Status.WAITLISTED) {
@@ -34,7 +36,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
return (
<div
className={`w-full flex justify-center rounded p-2 ${fontColor}`}
className={clsx('w-full flex justify-center rounded p-2', fontColor, className)}
style={{
backgroundColor: colors.primaryColor,
}}

View File

@@ -1,12 +1,14 @@
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import html2canvas from 'html2canvas';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
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 CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
import styles from './CalendarGrid.module.scss';
// import calIcon from 'src/assets/icons/cal.svg';
// import pngIcon from 'src/assets/icons/png.svg';
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 hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
@@ -34,13 +36,27 @@ interface Props {
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props> ): JSX.Element {
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
const [iterator, setIterator] = useState<number>(0);
const calendarRef = useRef(null); // Create a ref for the calendar grid
const saveAsPNG = () => {
if (calendarRef.current) {
html2canvas(calendarRef.current).then(canvas => {
// Create an a element to trigger download
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'calendar.png';
a.click();
});
}
};
return (
<div className={styles.calendar}>
<div className={styles.dayLabelContainer} />
{/* Displaying the rest of the calendar */}
<div className={styles.timeAndGrid}>
<div ref={calendarRef} className={styles.timeAndGrid}>
{/* <div className={styles.timeColumn}>
<div className={styles.timeBlock}></div>
{hoursOfDay.map((hour) => (
@@ -81,18 +97,18 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
</div>
</div>
{/* <div className={styles.buttonContainer}>
<div className={styles.divider}></div>
<div className={styles.buttonContainer}>
<div className={styles.divider} /> {/* First divider */}
<button className={styles.calendarButton}>
<img src={calIcon} className={styles.buttonIcon} alt="CAL" />
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
Save as .CAL
</button>
<div className={styles.divider}></div>
<button className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt="PNG" />
<div className={styles.divider} /> {/* Second divider */}
<button onClick={saveAsPNG} className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
Save as .PNG
</button>
</div> */}
</div>
</div>
);
}

View File

@@ -12,40 +12,41 @@ import ScheduleTotalHoursAndCourses from '../ScheduleTotalHoursAndCourses/Schedu
import CourseStatus from '../CourseStatus/CourseStatus';
const CalendarHeader = () => (
<div
style={{
display: 'flex',
minWidth: '672px',
minHeight: '79px',
padding: '15px 0px',
justifyContent: 'space-between',
alignItems: 'center',
alignContent: 'center',
rowGap: '10px',
alignSelf: 'stretch',
flexWrap: 'wrap',
}}
>
<div className='min-h-79px min-w-672px flex px-0 py-15'>
<div className='flex flex-row gap-20'>
<div className='flex gap-10'>
<div className='flex gap-1'>
<Button variant='single' icon={MenuIcon} color='ut-gray' />
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className='flex items-center'>
<LogoIcon style={{ marginRight: '5px' }} />
<Text>Your Logo Text</Text>
<div className='flex flex-col gap-1 whitespace-nowrap'>
<Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'>
UT Registration
</Text>
<Text className='leading-trim text-cap font-roboto text-base text-ut-orange font-medium'>
Plus
</Text>
</div>
</div>
</div>
<div className='flex flex-col'>
<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 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' />
<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>
<Divider type='solid' />
</div>
);

View File

@@ -21,7 +21,7 @@ export default function ScheduleTotalHoursAndCourses({
totalCourses,
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
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'>
{`${scheduleName}: `}
</Text>

View File

@@ -1,7 +1,7 @@
import { CalendarCourseCellProps } from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell';
import useSchedules from './useSchedules';
const dayToNumber = {
const dayToNumber: { [day: string]: number } = {
Monday: 0,
Tuesday: 1,
Wednesday: 2,
@@ -15,18 +15,26 @@ interface CalendarGridPoint {
endIndex: number;
}
/**
* Return type of useFlattenedCourseSchedule
*/
export interface CalendarGridCourse {
calendarGridPoint?: CalendarGridPoint;
calendarGridPoint: CalendarGridPoint;
componentProps: CalendarCourseCellProps;
}
const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30);
export function useFlattenedCourseSchedule() {
/**
* Get the active schedule, and convert it to be render-able into a calendar.
* @returns CalendarGridCourse
*/
export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
const [activeSchedule] = useSchedules();
const { courses } = activeSchedule;
const out = courses.flatMap(course => {
return courses
.flatMap(course => {
const {
status,
department,
@@ -39,10 +47,16 @@ export function useFlattenedCourseSchedule() {
// asynch, online course
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',
},
@@ -50,36 +64,39 @@ export function useFlattenedCourseSchedule() {
},
];
}
// in-person
return meetings.flatMap(meeting => {
const { days, startTime, endTime, location } = meeting;
const time = meeting.getTimeString({ separator: ' - ', capitalize: true });
const time = meeting.getTimeString({ separator: '-', capitalize: true });
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
return days.map(d => {
const dayIndex = dayToNumber[d];
const startIndex = convertMinutesToIndex(startTime);
const endIndex = convertMinutesToIndex(endTime);
const calendarGridPoint: CalendarGridPoint = {
dayIndex,
startIndex,
endIndex,
};
return {
calendarGridPoint,
return days.map(d => ({
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',
},
},
};
}));
});
})
.sort((a: CalendarGridCourse, b: CalendarGridCourse) => {
if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) {
return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex;
}
if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) {
return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex;
}
return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex;
});
});
return out;
}