Skip to content
This repository has been archived by the owner on Aug 27, 2024. It is now read-only.

Commit

Permalink
Add initial schedule page
Browse files Browse the repository at this point in the history
  • Loading branch information
anli5005 committed Dec 31, 2023
1 parent 9057ba7 commit 20aa1ca
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 22 deletions.
2 changes: 1 addition & 1 deletion app/assignments/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const formattedAssessments = allAssessments.map(a => {
{
name: "Scheduled",
date,
specifyTime: true,
specifyTime: false,
}
],
sortDate: date,
Expand Down
15 changes: 10 additions & 5 deletions app/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export const menuItems: MenuItemProps[] = [
icon: "📄",
href: "/syllabus",
},
{
id: "schedule",
title: "Schedule",
icon: "📅",
href: "/schedule",
},
{
id: "assignments",
title: "Assignments",
Expand Down Expand Up @@ -53,12 +59,12 @@ const MenuContext = createContext<MenuCoordinator | null>(null)

export function MenuContextProvider({ children }: { children: ReactNode }) {
const pathname = usePathname()
const [activeState, setActiveState] = useState<{ item: string | null, pathname: string }>({ item: null, pathname })
const [activeState, setActiveState] = useState<string | null>(null)

return <MenuContext.Provider value={{
activeItem: activeState.pathname === pathname ? activeState.item : null,
activeItem: activeState,
setActiveItem(item) {
setActiveState({ item, pathname })
setActiveState(item)
}
}}>
{children}
Expand All @@ -78,10 +84,9 @@ export type MenuItemActivatorProps = {

export function MenuItemActivator({ item }: MenuItemActivatorProps) {
const context = useMenuContext()
const pathname = usePathname()
useEffect(() => {
context.setActiveItem(item)
}, [pathname])
}, [])

return null
}
Expand Down
46 changes: 46 additions & 0 deletions app/schedule/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client"

import { Card } from "@/components/Card"
import { ScheduleTable, getSchedule } from "@/components/Schedule"
import { useEffect, useState } from "react"
import sections from "@/sections"

export function SchedulePageContent() {
const [section, setSection] = useState<string | null>(null)
const [isClient, setIsClient] = useState(false)

useEffect(() => {
setSection(localStorage.getItem("cis1951.section.24sp"))
setIsClient(true)
}, [])

const setSectionAndStore = (section: string) => {
localStorage.setItem("cis1951.section.24sp", section)
setSection(section)
}

if (!isClient) return null

if (!section) {
return <div className="flex flex-col items-center justify-center h-full gap-4">
<div className="text-2xl font-bold">Please choose a section:</div>
<div className="flex gap-4 text-xl">
{sections.map(section => <button key={section} className="link" onClick={() => setSectionAndStore(section)}>{section}</button>)}
</div>
<div className="opacity-70">You can change this later.</div>
</div>
}

const schedule = getSchedule(section)

return <div>
<Card margin>
You're currently viewing the schedule for section <strong>{section}</strong>. <button className="link" onClick={() => {
setSection(null)
}}>Switch section...</button>
</Card>
<Card title={<h1>Schedule</h1>}>
<ScheduleTable items={schedule} />
</Card>
</div>
}
13 changes: 13 additions & 0 deletions app/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Card } from "@/components/Card";
import { MenuItemActivator } from "../menu";
import { SchedulePageContent } from "./content";

export default function SchedulePage() {
return <>
<MenuItemActivator item="schedule" />
<SchedulePageContent />
<noscript>
<Card>You need to enable JavaScript to view the schedule.</Card>
</noscript>
</>
}
98 changes: 98 additions & 0 deletions components/Schedule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { allAssessments, allHomework, allLectures } from "contentlayer/generated"
import { ReactNode } from "react"
import { FormattedDate } from "./FormattedDate"
import Link from "next/link"

export type LectureScheduleItemDetails = {
type: "lecture"
title: string
body: typeof allLectures[0]["body"]
}

export type AssignmentScheduleItemDetails = {
type: "assignment"
title: string
event?: string
href: string
isReleased: boolean
releaseDate?: Date
}

export type ScheduleItem = {
date: Date
} & (LectureScheduleItemDetails | AssignmentScheduleItemDetails)

export function getSchedule(section: string | null): ScheduleItem[] {
const homework = allHomework.flatMap(hw => {
const shared = {
type: "assignment" as const,
title: hw.title,
href: `/assignments/hw/${hw.slug}`,
isReleased: hw.isReleased,
releaseDate: hw.releaseDate && new Date(hw.releaseDate),
}

return [
{
...shared,
event: "Due",
date: new Date(hw.dueDate),
},
...(hw.auxiliaryDates ?? []).map(aux => ({
...shared,
event: aux.name,
date: new Date(aux.date),
})),
]
})

const assessments = allAssessments.map(a => ({
type: "assignment" as const,
title: a.title,
href: `/assignments/assessment/${a.slug}`,
isReleased: a.isReleased,
releaseDate: a.releaseDate && new Date(a.releaseDate),
date: new Date(a.assessmentDate),
}))

const lectures = allLectures.map(l => ({
type: "lecture" as const,
title: l.title,
body: l.body,
date: l.dates[section] && new Date(l.dates[section]),
})).filter(l => l.date)

const schedule = [...homework, ...assessments, ...lectures]
return schedule.toSorted((a, b) => a.date.getTime() - b.date.getTime())
}

export function ScheduleRow({ date, ...details }: ScheduleItem) {
let content: ReactNode = null
if (details.type === "lecture") {
content = <>🧑‍🏫 {details.title}</>
} else if (details.type === "assignment") {
const title = details.isReleased ?
<Link href={details.href} className="link font-bold">{details.title}</Link> :
<span className="italic">{details.title}{details.releaseDate && <> (available <strong><FormattedDate date={details.releaseDate} format="M/d" /></strong>)</>}</span>
content = <span className={details.isReleased ? "" : "opacity-60"}>📋 {title}<span className="italic">{details.event && ` - ${details.event}`} @ <FormattedDate date={date} format="h:mmaa" /></span></span>
}

return <tr className="border-b border-neutral-300 dark:border-neutral-600">
<td className="text-left py-2 font-bold"><FormattedDate date={date} format="M/d" /></td>
<td className="text-left py-2">{content}</td>
</tr>
}

export function ScheduleTable({ items }: { items: ScheduleItem[] }) {
return <table className="w-full">
<thead>
<tr className="border-b-4 border-neutral-300 dark:border-neutral-600">
<th className="text-left pb-2 min-w-16">Date</th>
<th className="text-left pb-2 w-full">Event</th>
</tr>
</thead>
<tbody className="lg:table-row-group">
{items.map((item, index) => <ScheduleRow {...item} key={index} />)}
</tbody>
</table>
}
2 changes: 1 addition & 1 deletion components/StaffGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type StaffMemberProps = {
name: string
section?: "501" | "502"
section?: "201" | "202"
flavorText?: string
pennkey: string
school: "sas" | "seas" | "wharton" // Sorry, forgot what Penn Medicine has
Expand Down
20 changes: 13 additions & 7 deletions components/UpcomingAssignments.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { formattedAssessments, formattedHomework, AssignmentTable } from "@/app/assignments/page"
"use client"

const threshold = 3 * 7 * 24 * 60 * 60 * 1000 // 3 weeks
import { useState } from "react"
import { ScheduleTable, getSchedule } from "./Schedule"

const threshold = 5 * 7 * 24 * 60 * 60 * 1000 // 3 weeks
const thresholdText = "3 weeks"

const upcoming = [...formattedHomework, ...formattedAssessments].filter(({ sortDate }) => {
const remainingTime = sortDate.getTime() - new Date().getTime()
return remainingTime >= 0 && remainingTime <= threshold
})
const schedule = getSchedule(null)

export function UpcomingAssignments() {
const [now] = useState(new Date())
const upcoming = schedule.filter(item => {
const diff = item.date.getTime() - now.getTime()
return diff >= 0 && diff < threshold
})

if (!upcoming.length) return <div>
There are no upcoming assignments in the next {thresholdText}.
</div>

return <>
<div className="mb-4">There are {upcoming.length} {upcoming.length === 1 ? "assignment" : "assignments"} in the next {thresholdText}:</div>
<AssignmentTable assignments={upcoming} />
<ScheduleTable items={upcoming} />
</>
}
6 changes: 6 additions & 0 deletions content/lectures/01-intro.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "Lecture 1: Intro to iOS & Xcode"
dates:
"201": 2024-01-18T19:00:00-05:00
"202": 2024-01-22T17:15:00-05:00
---
8 changes: 3 additions & 5 deletions content/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ customLayout: true
</Card>

<Card title={<h2>Upcoming Assignments</h2>} margin>
**NOTE:** This component is currently broken. It only shows upcoming assignments at page build time, not at view time.

<UpcomingAssignments />
</Card>

Expand All @@ -21,14 +19,14 @@ customLayout: true
<StaffGrid members={[
{
name: "Jordan Hawkman",
section: "501",
section: "201",
pennkey: "jhawk24",
school: "seas",
pronouns: "he/him",
},
{
name: "Anthony Li",
section: "501",
section: "201",
flavorText: "OvercomplicatedView",
pennkey: "antli",
school: "seas",
Expand All @@ -39,7 +37,7 @@ customLayout: true
},
{
name: "Yuying Fan",
section: "502",
section: "202",
pennkey: "yuyingf",
school: "seas",
pronouns: "she/her",
Expand Down
21 changes: 19 additions & 2 deletions contentlayer.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineDocumentType, makeSource } from 'contentlayer/source-files'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer/source-files'
import rehypePrism from '@mapbox/rehype-prism'
import sections from './sections.json'

export const Page = defineDocumentType(() => ({
name: 'Page',
Expand Down Expand Up @@ -65,13 +66,29 @@ export const Assessment = defineDocumentType(() => ({
},
}))

const LectureDates = defineNestedType(() => {
const fields = {}
sections.forEach(section => {
fields[section] = { type: 'date', required: false }
})

return {
name: 'LectureDates',
fields,
}
})

export const Lecture = defineDocumentType(() => ({
name: 'Lecture',
filePathPattern: `lectures/**/*.mdx`,
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
dates: {
type: 'nested',
of: LectureDates,
required: true,
},
},
computedFields: {
slug: { type: 'string', resolve: page => page._raw.flattenedPath.slice("lectures/".length) },
Expand Down
1 change: 1 addition & 0 deletions sections.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["201", "202"]
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"@/app/*": ["app/*"],
"@/components/*": ["components/*"],
"@/styles/*": ["styles/*"],
"contentlayer/generated": ["./.contentlayer/generated"]
"contentlayer/generated": ["./.contentlayer/generated"],
"@/sections": ["sections.json"],
},
},
"include": [
Expand Down

0 comments on commit 20aa1ca

Please sign in to comment.