Skip to content

Commit

Permalink
feat: add calendar logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jsun969 committed Sep 19, 2024
1 parent e017a28 commit 2c0f1ad
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 9 deletions.
186 changes: 186 additions & 0 deletions __tests__/calendar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { getStartEndWeek, getWeekCourses } from '../src/helpers/calendar';
import dayjs from '../src/lib/dayjs';
import type { DetailedEnrolledCourse, WeekCourses } from '../src/types/course';

describe('getStartEndWeek', () => {
it('should return the first date (Monday) of the start and end week', () => {
const [start, end] = getStartEndWeek([
{ start: '09-18', end: '10-12' },
{ start: '11-13', end: '12-01' },
{ start: '12-12', end: '11-11' },
]);
expect(start.format('MM-DD')).toBe('09-16');
expect(end.format('MM-DD')).toBe('11-25');
});
});

describe('getWeekCourses', () => {
it('should return the courses for each day of the week', () => {
const enrolledCourses: Array<DetailedEnrolledCourse> = [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classes: [
{
type: 'Lecture',
id: 'l',
meetings: [
{
location: 'bragg',
day: 'Tuesday',
date: { start: '09-09', end: '09-27' },
time: { start: '09:00', end: '10:00' },
},
],
},
],
},
{
id: 'cs',
name: { code: 'cs', subject: 'cs', title: 'compsci' },
classes: [
{
type: 'Practical',
id: 'p',
meetings: [
{
location: 'online',
day: 'Monday',
date: { start: '09-09', end: '09-27' },
time: { start: '17:00', end: '18:00' },
},
],
},
{
type: 'Workshop',
id: 'w',
meetings: [
{
location: 'iw',
day: 'Friday',
date: { start: '09-09', end: '09-27' },
time: { start: '09:00', end: '10:00' },
},
],
},
],
},
];
const courses = getWeekCourses(dayjs('2024-09-16'), enrolledCourses);
const expectedRes: WeekCourses = {
Monday: [
{
id: 'cs',
name: { code: 'cs', subject: 'cs', title: 'compsci' },
classId: 'p',
classType: 'Practical',
location: 'online',
time: { start: '17:00', end: '18:00' },
},
],
Tuesday: [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classId: 'l',
classType: 'Lecture',
location: 'bragg',
time: { start: '09:00', end: '10:00' },
},
],
Wednesday: [],
Thursday: [],
Friday: [
{
id: 'cs',
name: { code: 'cs', subject: 'cs', title: 'compsci' },
classId: 'w',
classType: 'Workshop',
location: 'iw',
time: { start: '09:00', end: '10:00' },
},
],
};
expect(courses).toEqual(expectedRes);
});
it('should return the courses if course start at the end of the week', () => {
const enrolledCourses: Array<DetailedEnrolledCourse> = [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classes: [
{
type: 'Lecture',
id: 'l',
meetings: [
{
location: 'bragg',
day: 'Friday',
date: { start: '09-20', end: '10-04' },
time: { start: '09:00', end: '10:00' },
},
],
},
],
},
];
const courses = getWeekCourses(dayjs('2024-09-16'), enrolledCourses);
const expectedRes: WeekCourses = {
Monday: [],
Tuesday: [],
Wednesday: [],
Thursday: [],
Friday: [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classId: 'l',
classType: 'Lecture',
location: 'bragg',
time: { start: '09:00', end: '10:00' },
},
],
};
expect(courses).toEqual(expectedRes);
});
it('should return the courses if course end at the start of the week', () => {
const enrolledCourses: Array<DetailedEnrolledCourse> = [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classes: [
{
type: 'Lecture',
id: 'l',
meetings: [
{
location: 'bragg',
day: 'Monday',
date: { start: '08-12', end: '09-16' },
time: { start: '09:00', end: '10:00' },
},
],
},
],
},
];
const courses = getWeekCourses(dayjs('2024-09-16'), enrolledCourses);
const expectedRes: WeekCourses = {
Monday: [
{
id: 'm',
name: { code: 'm', subject: 'm', title: 'math' },
classId: 'l',
classType: 'Lecture',
location: 'bragg',
time: { start: '09:00', end: '10:00' },
},
],
Tuesday: [],
Wednesday: [],
Thursday: [],
Friday: [],
};
expect(courses).toEqual(expectedRes);
});
});
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Calendar } from './components/Calendar';
import { EnrolledCourses } from './components/EnrolledCourses';
import { Header } from './components/Header';
import { SearchForm } from './components/SearchForm';
Expand All @@ -12,6 +13,7 @@ export const App = () => {
<main className="mx-auto my-4 max-w-screen-xl space-y-4 px-2">
<SearchForm />
<EnrolledCourses />
<Calendar />
</main>
</>
);
Expand Down
33 changes: 33 additions & 0 deletions src/components/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button } from '@nextui-org/react';
import { useEffect } from 'react';

import { useCalendar } from '../helpers/calendar';

export const Calendar = () => {
const { courses, currentWeek, nextWeek, prevWeek } = useCalendar();
useEffect(() => {
console.log(courses);
});

return (
<div>
<h1>{currentWeek.format('MMMM D, YYYY')}</h1>
<Button
isIconOnly
variant="light"
className="text-2xl"
onClick={prevWeek}
>
⬅️
</Button>
<Button
isIconOnly
variant="light"
className="text-2xl"
onClick={nextWeek}
>
➡️
</Button>
</div>
);
};
2 changes: 1 addition & 1 deletion src/data/course-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ export const useCoursesInfo = () => {
queryFn: () => getCourse({ id }),
})),
});
return data.map((d) => d.data).filter((d) => d !== undefined);
return data.map((d) => d.data).filter(Boolean) as Course[];
};
115 changes: 115 additions & 0 deletions src/helpers/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useEffect, useState } from 'react';

import { useDetailedEnrolledCourses } from '../data/enrolled-courses';
import dayjs from '../lib/dayjs';
import type {
DetailedEnrolledCourse,
WeekCourse,
WeekCourses,
} from '../types/course';
import { dateToDayjs, getMonday } from '../utils/date';

const MAX_DATE = dayjs('6666-06-06');
const MIN_DATE = dayjs('2005-03-12');

/**
* Get the start date (Monday) of the start and end week
* @param dates Dates for all enrolled meetings
* @returns Tuple of dates of the start and end week
*/
export const getStartEndWeek = (
dates: Array<{ start: string; end: string }>,
): [dayjs.Dayjs, dayjs.Dayjs] => {
const currentMonday = getMonday(dayjs());
if (dates.length === 0) return [currentMonday, currentMonday];

let startWeek = MAX_DATE;
let endWeek = MIN_DATE;
dates.forEach((date) => {
const start = dateToDayjs(date.start);
const end = dateToDayjs(date.end);
if (start.isBefore(startWeek)) {
startWeek = start;
}
if (end.isAfter(endWeek)) {
endWeek = end;
}
});

return [getMonday(startWeek), getMonday(endWeek)];
};

/**
* Get courses for each day of the week
* @param weekStart Start of the week (Monday)
* @param enrolledCourses All detailed enrolled courses
* @returns Object with courses for each day of the week
*/
export const getWeekCourses = (
weekStart: dayjs.Dayjs,
enrolledCourses: Array<DetailedEnrolledCourse>,
): WeekCourses => {
const weekEnd = weekStart.add(4, 'days');
const courses: WeekCourses = {
Monday: [],
Tuesday: [],
Wednesday: [],
Thursday: [],
Friday: [],
};

enrolledCourses.forEach((c) => {
c.classes.forEach((cl) => {
cl.meetings.forEach((m) => {
const isMeetingInWeek =
weekEnd.isSameOrAfter(dateToDayjs(m.date.start)) &&
weekStart.isSameOrBefore(dateToDayjs(m.date.end));
if (!isMeetingInWeek) return;
const course: WeekCourse = {
id: c.id,
name: c.name,
classId: cl.id,
classType: cl.type,
location: m.location,
time: m.time,
};
courses[m.day].push(course);
});
});
});

return courses;
};

export const useCalendar = () => {
const enrolledCourses = useDetailedEnrolledCourses();

const dates = enrolledCourses.flatMap((c) =>
c.classes.flatMap((cl) => cl.meetings.flatMap((m) => m.date)),
);
const [startWeek, endWeek] = getStartEndWeek(dates);

const [currentWeek, setCurrentWeek] = useState(getMonday(dayjs()));

useEffect(() => {
if (currentWeek.isBefore(startWeek)) {
setCurrentWeek(startWeek);
}
if (currentWeek.isAfter(endWeek)) {
setCurrentWeek(endWeek);
}
}, [startWeek, endWeek, currentWeek]);

const nextWeek = () => {
if (currentWeek.isSame(endWeek)) return;
setCurrentWeek((c) => c.add(1, 'week'));
};
const prevWeek = () => {
if (currentWeek.isSame(startWeek)) return;
setCurrentWeek((c) => c.subtract(1, 'week'));
};

const courses = getWeekCourses(currentWeek, enrolledCourses);

return { currentWeek, nextWeek, prevWeek, courses };
};
4 changes: 4 additions & 0 deletions src/lib/dayjs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import isoWeek from 'dayjs/plugin/isoWeek';

dayjs.extend(customParseFormat);
dayjs.extend(isoWeek);
dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);

export default dayjs;
2 changes: 2 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { mountStoreDevtool } from 'simple-zustand-devtools';
import { Toaster } from 'sonner';

import { App } from './App';
import { useEnrolledCourses } from './data/enrolled-courses';
Expand All @@ -29,6 +30,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<NextUIProvider>
<Toaster richColors position="top-center" />
<App />
</NextUIProvider>
</QueryClientProvider>
Expand Down
Loading

0 comments on commit 2c0f1ad

Please sign in to comment.