feat: show async courses in the bottom bar (#204)

* feat: show async courses in the bottom bar

* fix: hide "Async/Other" header when there are no async courses, off-by-n error

(where n is the number async courses)

* refactor: move types closer to map instead of weird "as boolean"

* refactor: move satisfies to return type
This commit is contained in:
Samuel Gunter
2024-03-29 00:01:23 -05:00
committed by GitHub
parent d3f64ec79e
commit 227de53e84
6 changed files with 84 additions and 62 deletions

View File

@@ -84,18 +84,29 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
courses: [ courseCells: [
{ {
colors: getCourseColors('pink', 200), async: true,
courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} ${exampleGovCourse.instructors[0]!.lastName}`, calendarGridPoint: { dayIndex: -1, endIndex: -1, startIndex: -1 },
status: exampleGovCourse.status, componentProps: {
colors: getCourseColors('pink', 200),
courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} ${exampleGovCourse.instructors[0]!.lastName}`,
status: exampleGovCourse.status,
},
course: exampleGovCourse,
}, },
{ {
colors: getCourseColors('slate', 500), async: true,
courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} ${examplePsyCourse.instructors[0]!.lastName}`, calendarGridPoint: { dayIndex: -1, endIndex: -1, startIndex: -1 },
status: examplePsyCourse.status, componentProps: {
colors: getCourseColors('slate', 500),
courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} ${examplePsyCourse.instructors[0]!.lastName}`,
status: examplePsyCourse.status,
},
course: examplePsyCourse,
}, },
], ],
setCourse: () => {},
}, },
render: props => ( render: props => (
<div className='outline-red outline w-292.5!'> <div className='outline-red outline w-292.5!'>
@@ -105,7 +116,7 @@ export const Default: Story = {
}; };
export const Empty: Story = { export const Empty: Story = {
args: { args: {
courses: [], courseCells: [],
}, },
render: props => ( render: props => (
<div className='outline-red outline w-292.5!'> <div className='outline-red outline w-292.5!'>

View File

@@ -35,6 +35,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -49,6 +50,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -63,6 +65,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -77,6 +80,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -91,6 +95,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -105,6 +110,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
{ {
calendarGridPoint: { calendarGridPoint: {
@@ -119,6 +125,7 @@ const testData: CalendarGridCourse[] = [
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
}, },
course: ExampleCourse, course: ExampleCourse,
async: false,
}, },
]; ];

View File

@@ -87,7 +87,7 @@ export default function Calendar(): JSX.Element {
<div className='min-h-2xl flex-grow overflow-auto pl-2 pr-4 pt-6 screenshot:min-h-xl'> <div className='min-h-2xl flex-grow overflow-auto pl-2 pr-4 pt-6 screenshot:min-h-xl'>
<CalendarGrid courseCells={courseCells} setCourse={setCourse} /> <CalendarGrid courseCells={courseCells} setCourse={setCourse} />
</div> </div>
<CalendarBottomBar /> <CalendarBottomBar courseCells={courseCells} setCourse={setCourse} />
</div> </div>
</div> </div>

View File

@@ -1,18 +1,20 @@
import type { Course } from '@shared/types/Course';
import { saveAsCal, saveCalAsPng } from '@views/components/calendar/utils'; import { saveAsCal, saveCalAsPng } from '@views/components/calendar/utils';
import { Button } from '@views/components/common/Button'; import { Button } from '@views/components/common/Button';
import Divider from '@views/components/common/Divider'; import Divider from '@views/components/common/Divider';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
import ImageIcon from '~icons/material-symbols/image'; import ImageIcon from '~icons/material-symbols/image';
import type { CalendarCourseCellProps } from './CalendarCourseCell';
import CalendarCourseBlock from './CalendarCourseCell'; import CalendarCourseBlock from './CalendarCourseCell';
type CalendarBottomBarProps = { type CalendarBottomBarProps = {
courses?: CalendarCourseCellProps[]; courseCells?: CalendarGridCourse[];
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
}; };
/** /**
@@ -21,8 +23,9 @@ type CalendarBottomBarProps = {
* @param {Object[]} courses - The list of courses to display in the calendar. * @param {Object[]} courses - The list of courses to display in the calendar.
* @returns {JSX.Element} The rendered bottom bar component. * @returns {JSX.Element} The rendered bottom bar component.
*/ */
export default function CalendarBottomBar({ courses }: CalendarBottomBarProps): JSX.Element { export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBottomBarProps): JSX.Element {
const displayCourses = courses && courses.length > 0; const asyncCourseCells = courseCells?.filter(block => block.async);
const displayCourses = asyncCourseCells && asyncCourseCells.length > 0;
return ( return (
<div className='w-full flex py-1.25 pl-7.5 pr-6.25'> <div className='w-full flex py-1.25 pl-7.5 pr-6.25'>
@@ -35,15 +38,19 @@ export default function CalendarBottomBar({ courses }: CalendarBottomBarProps):
<> <>
<Text variant='h4'>Async/Other:</Text> <Text variant='h4'>Async/Other:</Text>
<div className='inline-flex gap-2.5'> <div className='inline-flex gap-2.5'>
{courses.map(({ courseDeptAndInstr, status, colors, className }) => ( {asyncCourseCells.map(block => {
<CalendarCourseBlock const { courseDeptAndInstr, status, colors, className } = block.componentProps;
courseDeptAndInstr={courseDeptAndInstr} return (
status={status} <CalendarCourseBlock
colors={colors} courseDeptAndInstr={courseDeptAndInstr}
key={courseDeptAndInstr} status={status}
className={clsx(className, 'w-35! h-15!')} colors={colors}
/> key={courseDeptAndInstr}
))} className={clsx(className, 'w-35! h-15!')}
onClick={() => setCourse(block.course)}
/>
);
})}
</div> </div>
</> </>
)} )}

View File

@@ -123,28 +123,30 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
}); });
}); });
return courseCells.map((block, i) => { return courseCells
const { courseDeptAndInstr, timeAndLocation, status } = courseCells[i]!.componentProps; .filter(block => !block.async)
.map(block => {
const { courseDeptAndInstr, timeAndLocation, status } = block.componentProps;
return ( return (
<div <div
key={`${JSON.stringify(block)}`} key={`${JSON.stringify(block)}`}
style={{ style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 3}`, gridColumn: `${block.calendarGridPoint.dayIndex + 3}`,
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`, gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
width: `calc(100% / ${block.totalColumns ?? 1})`, width: `calc(100% / ${block.totalColumns ?? 1})`,
marginLeft: `calc(100% * ${((block.gridColumnStart ?? 0) - 1) / (block.totalColumns ?? 1)})`, marginLeft: `calc(100% * ${((block.gridColumnStart ?? 0) - 1) / (block.totalColumns ?? 1)})`,
}} }}
className='pb-1 pl-0 pr-2.5 pt-0 screenshot:pb-0.5 screenshot:pr-0.5' className='pb-1 pl-0 pr-2.5 pt-0 screenshot:pb-0.5 screenshot:pr-0.5'
> >
<CalendarCourseCell <CalendarCourseCell
courseDeptAndInstr={courseDeptAndInstr} courseDeptAndInstr={courseDeptAndInstr}
timeAndLocation={timeAndLocation} timeAndLocation={timeAndLocation}
status={status} status={status}
colors={block.course.colors} colors={block.course.colors}
onClick={() => setCourse(block.course)} onClick={() => setCourse(block.course)}
/> />
</div> </div>
); );
}); });
} }

View File

@@ -1,7 +1,5 @@
import type { HexColor } from '@shared/types/Color';
import type { Course, StatusType } from '@shared/types/Course'; import type { Course, StatusType } from '@shared/types/Course';
import type { CourseMeeting } from '@shared/types/CourseMeeting'; import type { CourseMeeting } from '@shared/types/CourseMeeting';
import { colors } from '@shared/types/ThemeColors';
import type { UserSchedule } from '@shared/types/UserSchedule'; import type { UserSchedule } from '@shared/types/UserSchedule';
import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell'; import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell';
@@ -34,6 +32,7 @@ export interface CalendarGridCourse {
calendarGridPoint: CalendarGridPoint; calendarGridPoint: CalendarGridPoint;
componentProps: CalendarCourseCellProps; componentProps: CalendarCourseCellProps;
course: Course; course: Course;
async: boolean;
gridColumnStart?: number; gridColumnStart?: number;
gridColumnEnd?: number; gridColumnEnd?: number;
totalColumns?: number; totalColumns?: number;
@@ -103,25 +102,23 @@ function processAsyncCourses({
courseDeptAndInstr: string; courseDeptAndInstr: string;
status: StatusType; status: StatusType;
course: Course; course: Course;
}) { }): CalendarGridCourse[] {
return [ return [
{ {
calendarGridPoint: { calendarGridPoint: {
dayIndex: 0, dayIndex: -1,
startIndex: 0, startIndex: -1,
endIndex: 0, endIndex: -1,
}, },
componentProps: { componentProps: {
courseDeptAndInstr, courseDeptAndInstr,
status, status,
colors: { colors: course.colors,
primaryColor: colors.ut.gray as HexColor,
secondaryColor: colors.ut.gray as HexColor,
},
}, },
course, course,
async: true,
}, },
] satisfies CalendarGridCourse[]; ];
} }
/** /**
@@ -132,7 +129,7 @@ function processInPersonMeetings(
courseDeptAndInstr: string, courseDeptAndInstr: string,
status: StatusType, status: StatusType,
course: Course course: Course
) { ): CalendarGridCourse[] {
const { days, startTime, endTime, location } = meeting; const { days, startTime, endTime, location } = meeting;
const midnightIndex = 1440; const midnightIndex = 1440;
const normalizingTimeFactor = 720; const normalizingTimeFactor = 720;
@@ -151,13 +148,11 @@ function processInPersonMeetings(
courseDeptAndInstr, courseDeptAndInstr,
timeAndLocation, timeAndLocation,
status, status,
colors: { colors: course.colors,
primaryColor: colors.ut.orange as HexColor,
secondaryColor: colors.ut.orange as HexColor,
},
}, },
course, course,
})) satisfies CalendarGridCourse[]; async: false,
}));
} }
/** /**