Skip to content

Commit

Permalink
feat(web): exam ics (#655)
Browse files Browse the repository at this point in the history
  • Loading branch information
leomotors authored Nov 2, 2023
1 parent 9c7703a commit 8c22088
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/wise-bottles-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'web': minor
---

feat(web): download exam ics
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"escape-html": "^1.0.3",
"framer-motion": "7.1.0",
"html2canvas": "1.4.1",
"ics": "^3.5.0",
"isomorphic-dompurify": "0.20.0",
"md5": "2.3.0",
"mobx": "6.10.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/common/i18n/locales/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const shoppingPanel = {
export const schedulePage = {
title: 'จัดตารางเรียน',
downloadPng: 'PNG',
addToCalendar: 'Google Calendar',
addToCalendar: 'ตารางเรียน (Soon)',
showCR11: 'แสดง จท11',
showCR11Mobile: 'จท11',
sumCreditsDesc: 'หน่วยกิตรวมในตาราง',
Expand Down
105 changes: 105 additions & 0 deletions apps/web/src/modules/Schedule/ics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import toast from 'react-hot-toast'

import * as ics from 'ics'

import { CourseCartItem } from '@web/store'

import { ExamPeriod, Maybe } from '@cgr/codegen'

function createSchedule(
course: CourseCartItem,
period: Maybe<ExamPeriod> | undefined,
name: string
) {
if (!period?.date || !period.period?.start || !period.period?.end) {
return null
}

const date = new Date(period.date)
const start = period.period.start.split(':').map(Number)
const end = period.period.end.split(':').map(Number)

if (isNaN(start[0]) || isNaN(start[1]) || isNaN(end[0]) || isNaN(end[1])) {
console.error(
`Invalid time format for ${course.courseNo}: ${period.period.start} or ${period.period.end}`
)
return null
}

date.setUTCHours(start[0])
date.setUTCMinutes(start[1])

// We save BE year in Database as UTC 💀
date.setUTCFullYear(date.getUTCFullYear() - 543)
// Subtract 7 hours to make it UTC
date.setUTCHours(date.getUTCHours() - 7)

const minutes = (end[0] - start[0]) * 60 + (end[1] - start[1])

console.log({ utc: date.toISOString(), local: date.toLocaleString() })

return {
title: course.abbrName,
description: `${name} Exam ${course.courseNo} ${course.courseNameEn}`,
// No exam start before 7AM
start: [
date.getUTCFullYear(),
date.getUTCMonth() + 1,
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
],
startInputType: 'utc',
duration: { minutes },
// TODO add location
} satisfies ics.EventAttributes
}

function createSchedules(courses: CourseCartItem[]) {
const events: ics.EventAttributes[] = []

for (const course of courses) {
const { midterm, final } = course

const midtermExam = createSchedule(course, midterm, 'Midterm')
const finalExam = createSchedule(course, final, 'Final')

if (midtermExam) {
events.push(midtermExam)
}

if (finalExam) {
events.push(finalExam)
}
}

if (events.length === 0) {
toast.error('ไม่สามารถสร้าง ics ได้เนื่องจากไม่มีการสอบ')
return undefined
}

const { value, error } = ics.createEvents(events)

if (error) {
toast.error(`Failed to generate exam schedule: ${error}`)
}

return value
}

export function downloadExamSchedules(courses: CourseCartItem[]) {
const value = createSchedules(courses)

if (!value) {
return
}

const blob = new Blob([value], { type: 'text/calendar' })
const url = URL.createObjectURL(blob)

const anchor = document.createElement('a')
anchor.href = url
anchor.download = 'exam.ics'
anchor.click()
anchor.remove()
}
4 changes: 4 additions & 0 deletions apps/web/src/modules/Schedule/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
useTimetableClasses,
} from './components/Schedule/utils'
import { ScheduleTable } from './components/ScheduleTable'
import { downloadExamSchedules } from './ics'
import {
ButtonBar,
ExamContainer,
Expand Down Expand Up @@ -120,6 +121,9 @@ export const SchedulePage = observer(() => {
<Button variant="outlined" disabled>
{t('addToCalendar')}
</Button>
<Button variant="outlined" onClick={() => downloadExamSchedules(shopItems)}>
ตารางสอบ (.ics)
</Button>
<LinkWithAnalytics href={buildLink(`/schedule/cr11`)} passHref elementName={CR11_BUTTON}>
<Button style={{ marginRight: 0 }} variant="outlined">
{isDesktop ? t('showCR11') : t('showCR11Mobile')}
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8c22088

Please sign in to comment.