Compare commits

..

8 Commits

Author SHA1 Message Date
Vinson Zheng
0eefaa1860 Halfway Tailwind conversion 2024-02-17 19:37:45 -06:00
Lukas Zenick
e44b0c0e45 FIX README 2024-02-17 17:24:27 -06:00
Lukas Zenick
206c97c5b5 fixed README 2024-02-17 17:23:04 -06:00
ac71b838db feat: Derek vinson/calendar header (#94)
* CalendarHeader alignment progress

* Boom

* css

* Between

* Lol

* Gap fix

* whitespace-nowrap

* Gaps

* Finished alignment of CourseStatus and Buttons

* Colors

* ESLint auto format

* Color UT Registration Plus text

* Reverting vscode

---------

Co-authored-by: Vinson Zheng <vinsonzheng499@gmail.com>
2024-02-17 17:02:36 -06:00
Abhinav Chadaga
4f5753917b sort the flattened course schedule (#93)
first by dayIndex, then startIndex, then endIndex.
2024-02-17 16:57:51 -06:00
DhruvArora-03
478ab31706 Merge branch 'testing-useFlattenedCourseSchedule' into hackathon 2024-02-17 16:37:58 -06:00
Samuel Gunter
42420f5502 feat: bottom bar for the calendar page (#91) 2024-02-17 16:25:50 -06:00
Lukas Zenick
a03bcf17b8 broken file 2024-02-17 15:49:52 -06:00
11 changed files with 292 additions and 132 deletions

View File

@@ -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

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'; 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',

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

View File

@@ -1,12 +1,12 @@
import React, {useRef} from 'react'; import React, { useRef } from 'react';
import html2canvas from 'html2canvas'; 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 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);
@@ -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>
); );
@@ -34,12 +36,12 @@ 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) { if (calendarRef.current) {
html2canvas(calendarRef.current).then((canvas) => { html2canvas(calendarRef.current).then(canvas => {
// Create an a element to trigger download // Create an a element to trigger download
const a = document.createElement('a'); const a = document.createElement('a');
a.href = canvas.toDataURL('image/png'); a.href = canvas.toDataURL('image/png');
@@ -50,20 +52,9 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
}; };
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} />
@@ -85,19 +76,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>

View File

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

View File

@@ -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>

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

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