Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
pathToCheatSheet,
pathToCourseHome,
pathToCourseNotes,
pathToCourseResource,
pathToCourseView,
pathToHomework,
} from '@alea/utils';
import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import MenuBookOutlinedIcon from '@mui/icons-material/MenuBookOutlined';
import QuizOutlinedIcon from '@mui/icons-material/QuizOutlined';
Expand All @@ -24,6 +26,7 @@ export function CourseDashboardCard({
isLoading,
institutionId,
instance,
courseInfo,
}: CourseDashboardCardProps) {
const t = getLocaleObject(useRouter()).studentWelcomeScreen;
const courseHome = pathToCourseHome(institutionId, courseId, instance);
Expand All @@ -48,6 +51,11 @@ export function CourseDashboardCard({
icon: SlideshowOutlinedIcon,
label: t.slides,
},
{
href: pathToCheatSheet(institutionId, courseId, instance),
icon: ArticleOutlinedIcon,
label: t.cheatsheet,
},
];

if (isLoading || !data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import QuizOutlinedIcon from '@mui/icons-material/QuizOutlined';
import SchoolOutlinedIcon from '@mui/icons-material/SchoolOutlined';
import UploadFileOutlinedIcon from '@mui/icons-material/UploadFileOutlined';
import { Box, Typography } from '@mui/material';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
Expand Down Expand Up @@ -94,6 +95,21 @@ export function WhatsNextSection({ quickAccess }: WhatsNextSectionProps) {
courseId={quickAccess.nextAssignment.courseId}
/>
)}
{quickAccess.pendingCheatsheetUpload && (
<QuickAccessCard
href={quickAccess.pendingCheatsheetUpload.data.href}
icon={UploadFileOutlinedIcon}
title={t.cheatsheetUploadPending}
subtitle={
quickAccess.pendingCheatsheetUpload.data.windowEndTs
? `${t.uploadBefore} ${dayjs(
quickAccess.pendingCheatsheetUpload.data.windowEndTs
).format('MMM D, HH:mm')}`
: t.uploadPending
}
courseId={quickAccess.pendingCheatsheetUpload.courseId}
/>
)}
{quickAccess.nextLecture && !quickAccess.nextLecture.data.isOngoing && (
<QuickAccessCard
{...getScheduleLink(quickAccess.nextLecture.data, quickAccess.nextLecture.courseId)}
Expand Down
5 changes: 5 additions & 0 deletions packages/alea-frontend/components/StudentDashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export interface CourseStudentData {
nextTutorialVenue?: string;
nextTutorialVenueLink?: string;
isSemesterOver: boolean;
cheatsheetUploadPending?: boolean;
cheatsheetUploadHref?: string;
cheatsheetUploadWindowEndTs?: number;
}

export type QuickAccessItem<T> = {
Expand All @@ -43,6 +46,7 @@ export interface QuickAccessData {
venueLink?: string;
isOngoing?: boolean;
}> | null;
pendingCheatsheetUpload: QuickAccessItem<{ href: string; windowEndTs?: number }> | null;
}

export interface SemesterOverBannerProps {
Expand All @@ -66,6 +70,7 @@ export interface WhatsNextSectionProps {
export interface CourseDashboardCardProps {
courseId: string;
courseName: string;
courseInfo?: CourseInfo;
data: CourseStudentData | undefined;
isLoading: boolean;
institutionId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { HomeworkStub, LectureSchedule, LectureScheduleItem, QuizStubInfo } from '@alea/spec';
import {
getCheatSheets,
getCheatsheetUploadWindow,
getCourseQuizList,
getHomeworkList,
getLectureEntry,
getLectureSchedule,
getSemesterInfo,
} from '@alea/spec';
import type { CourseInfo } from '@alea/utils';
import { pathToCheatSheet } from '@alea/utils';
import { useQueries, useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { useMemo } from 'react';
import type { CourseStudentData } from './types';
import { DEFAULT_INSTITUTION } from './types';
import { getNextOrCurrentScheduleOccurrence, normalizeLectureScheduleEntry } from './utils';

dayjs.extend(utc);
Expand All @@ -28,7 +31,9 @@ function normalizeSchedule(items: LectureSchedule[] | undefined): LectureSchedul

async function fetchCourseDashboardData(
courseId: string,
currentTerm: string
currentTerm: string,
courseInfo: CourseInfo | undefined,
institutionId: string
): Promise<Omit<CourseStudentData, 'isSemesterOver'>> {
const now = Date.now();
const courseResult: Omit<CourseStudentData, 'isSemesterOver'> = {
Expand All @@ -45,9 +50,16 @@ async function fetchCourseDashboardData(
instanceId: currentTerm,
}).catch(() => null);

const [quizList, homeworkList] = await Promise.all([
const [quizList, homeworkList, uploadWindowResponse, cheatSheets] = await Promise.all([
getCourseQuizList(courseId),
getHomeworkList(courseId),
courseInfo?.cheatsheetConfig?.hasCheatsheet &&
courseInfo.cheatsheetConfig?.canStudentUploadCheatsheet
? getCheatsheetUploadWindow(courseId, currentTerm, institutionId).catch(() => null)
: Promise.resolve(null),
courseInfo?.cheatsheetConfig?.hasCheatsheet
? getCheatSheets(courseId, currentTerm).catch(() => [])
: Promise.resolve([]),
]);

const quizzes = (quizList || []) as QuizStubInfo[];
Expand Down Expand Up @@ -87,13 +99,35 @@ async function fetchCourseDashboardData(
courseResult.nextTutorialVenueLink = nextTutorial.venueLink;
}

if (
courseInfo?.cheatsheetConfig?.hasCheatsheet &&
courseInfo.cheatsheetConfig?.canStudentUploadCheatsheet
) {
const currentWindow = uploadWindowResponse?.currentWindow;
const sheets = (cheatSheets || []) as Array<{ weekId: string }>;

if (!currentWindow?.isWithinWindow) return courseResult;
if (currentWindow.isSkipped) return courseResult;

const currentWeekId = currentWindow.weekId;
const hasUploadedCurrentWeek = sheets.some((sheet) => sheet.weekId === currentWeekId);

if (hasUploadedCurrentWeek) return courseResult;

courseResult.cheatsheetUploadPending = true;
courseResult.cheatsheetUploadHref = pathToCheatSheet(institutionId, courseId, currentTerm);
courseResult.cheatsheetUploadWindowEndTs = currentWindow.windowEnd
? dayjs.utc(currentWindow.windowEnd).valueOf()
: undefined;
}

return courseResult;
}

async function getSemesterOver(currentTerm: string): Promise<boolean> {
async function getSemesterOver(currentTerm: string, institutionId: string): Promise<boolean> {
const now = Date.now();
try {
const raw = await getSemesterInfo(DEFAULT_INSTITUTION, currentTerm);
const raw = await getSemesterInfo(institutionId, currentTerm);
const obj = Array.isArray(raw) ? raw[0] : raw;
const endDate = (obj as { lectureEndDate?: string } | null)?.lectureEndDate;
return !!endDate && now > dayjs.utc(endDate).endOf('day').valueOf();
Expand All @@ -104,20 +138,32 @@ async function getSemesterOver(currentTerm: string): Promise<boolean> {

export function useStudentDashboardData(
enrolledCourseIds: string[],
currentTerm: string | undefined
currentTerm: string | undefined,
allCourses: Record<string, CourseInfo>,
institutionId: string
): { data: Record<string, CourseStudentData>; loading: boolean } {
const semesterQuery = useQuery({
queryKey: ['semester', DEFAULT_INSTITUTION, currentTerm],
queryFn: () => getSemesterOver(currentTerm!),
queryKey: ['semester', institutionId, currentTerm],
queryFn: () => getSemesterOver(currentTerm ?? '', institutionId),
enabled: !!currentTerm && enrolledCourseIds.length > 0,
});

const courseQueries = useQueries({
queries: enrolledCourseIds.map((courseId) => ({
queryKey: ['course-dashboard', courseId, currentTerm],
queryFn: () => fetchCourseDashboardData(courseId, currentTerm!),
enabled: !!currentTerm,
})),
queries: enrolledCourseIds.map((courseId) => {
const courseInfo = allCourses[courseId];
return {
queryKey: [
'course-dashboard',
courseId,
currentTerm,
courseInfo?.cheatsheetConfig?.hasCheatsheet,
courseInfo?.cheatsheetConfig?.canStudentUploadCheatsheet,
],
queryFn: () =>
fetchCourseDashboardData(courseId, currentTerm ?? '', courseInfo, institutionId),
enabled: !!currentTerm,
};
}),
});

const data = useMemo(() => {
Expand Down
15 changes: 14 additions & 1 deletion packages/alea-frontend/components/StudentDashboard/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { LectureSchedule, LectureScheduleItem } from '@alea/spec';
import type { CourseInfo } from '@alea/utils';
import { parseTimeString, toWeekdayIndex } from '@alea/utils';
import { parseTimeString, pathToCheatSheet, toWeekdayIndex } from '@alea/utils';
import dayjs from 'dayjs';
import type { CourseStudentData, QuickAccessData } from './types';
import { DEFAULT_INSTITUTION } from './types';

export function stripHtml(html: string): string {
if (typeof html !== 'string') return '';
Expand Down Expand Up @@ -77,6 +78,7 @@ export function getAggregatedQuickAccess(
nextAssignment: null,
nextLecture: null,
nextTutorial: null,
pendingCheatsheetUpload: null,
};

const liveQuizEntry = Object.entries(courseData).find(([, info]) => info?.liveQuiz);
Expand Down Expand Up @@ -149,6 +151,17 @@ export function getAggregatedQuickAccess(
},
};
}

if (info.cheatsheetUploadPending && !result.pendingCheatsheetUpload) {
result.pendingCheatsheetUpload = {
courseId,
courseName: name,
data: {
href: info.cheatsheetUploadHref ?? pathToCheatSheet(DEFAULT_INSTITUTION, courseId),
windowEndTs: info.cheatsheetUploadWindowEndTs,
},
};
}
}

return result;
Expand Down
10 changes: 8 additions & 2 deletions packages/alea-frontend/components/StudentWelcomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export default function StudentWelcomeScreen({
[enrolledCourseIds, allCourses]
);

const { data: courseData, loading } = useStudentDashboardData(enrolledCourseIds, currentTerm);
const { data: courseData, loading } = useStudentDashboardData(
enrolledCourseIds,
currentTerm,
allCourses,
DEFAULT_INSTITUTION
);
const quickAccess = useMemo(
() => getAggregatedQuickAccess(courseData, allCourses),
[courseData, allCourses]
Expand Down Expand Up @@ -105,10 +110,11 @@ export default function StudentWelcomeScreen({
key={course.courseId}
courseId={course.courseId}
courseName={course.courseName}
courseInfo={course}
data={courseData[course.courseId]}
isLoading={loading}
institutionId={DEFAULT_INSTITUTION}
instance="latest"
instance={currentTerm ?? 'latest'}
/>
))}
</Box>
Expand Down
5 changes: 5 additions & 0 deletions packages/alea-frontend/lang/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,11 @@ export const de = {
assignments: 'Hausaufgaben',
notes: 'Skript',
slides: 'Folien',
cheatsheet: 'Spickzettel',
cheatsheetUploadPending: 'Spickzettel-Upload ausstehend',
cheatsheetEnrollmentPending: 'Einschreiben erforderlich, um auf den Spickzettel zuzugreifen',
uploadBefore: 'Hochladen vor',
uploadPending: 'Upload ausstehend',
notEnrolled: 'Sie sind noch in keinem Kurs eingeschrieben',
},
semesterOverBanner: {
Expand Down
5 changes: 5 additions & 0 deletions packages/alea-frontend/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,11 @@ export const en = {
assignments: 'Assignments',
notes: 'Notes',
slides: 'Slides',
cheatsheet: 'Cheatsheet',
cheatsheetUploadPending: 'Cheatsheet upload pending',
cheatsheetEnrollmentPending: 'Get enrolled to access the CheatSheet',
uploadBefore: 'Upload before',
uploadPending: 'Upload pending',
notEnrolled: "You're not enrolled in any courses yet",
},
semesterOverBanner: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getLectureEntry,
getLectureSchedule,
getSemesterInfo,
LectureScheduleItem
LectureScheduleItem,
} from '@alea/spec';
import { SafeFTMLDocument } from '@alea/stex-react-renderer';
import {
Expand All @@ -23,7 +23,7 @@ import {
pathToInstructorDash,
pathToPracticeProblems,
pathToStudyBuddy,
ResourceName
ResourceName,
} from '@alea/utils';
import ArticleIcon from '@mui/icons-material/Article';
import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn';
Expand Down Expand Up @@ -629,7 +629,7 @@ const CourseHomePage: NextPage = () => {
const cheatSheetsLink = pathToCheatSheet(institutionId, courseId, instanceId);

const locale = router.locale || 'en';
const { home, courseHome: tCourseHome, calendarSection: tCal, quiz: q } = getLocaleObject(router);
const { home, courseHome: tCourseHome, calendarSection: tCal, quiz: q , studentWelcomeScreen: tCheatsheet} = getLocaleObject(router);
const t = home.courseThumb;

const showSearchBar = ['ai-1', 'ai-2', 'iwgs-1', 'iwgs-2'].includes(courseId);
Expand Down Expand Up @@ -776,12 +776,26 @@ const CourseHomePage: NextPage = () => {
<PersonIcon fontSize="large" />
</CourseComponentLink>
)}
{cheatsheetConfig?.hasCheatsheet && (
<CourseComponentLink href={cheatSheetsLink}>
CheatSheet&nbsp;
<FolderOpenIcon fontSize="large" />
</CourseComponentLink>
)}
{cheatsheetConfig?.hasCheatsheet &&
(enrolled ? (
<CourseComponentLink href={cheatSheetsLink}>
CheatSheet&nbsp;
<FolderOpenIcon fontSize="large" />
</CourseComponentLink>
) : (
<Tooltip title={tCheatsheet.cheatsheetEnrollmentPending} arrow>
<span style={{ width: '100%' }}>
<Button
variant="contained"
disabled
sx={{ width: '100%', height: '48px', fontSize: '16px' }}
>
CheatSheet&nbsp;
<FolderOpenIcon fontSize="large" />
</Button>
</span>
</Tooltip>
))}
</Box>
<InstructorDetails details={instructorDetails} />
{enrolled === false && !isSemesterOver && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const now = new Date();
for (const weekStart of weekStarts) {
const { windowStart, windowEnd } = getUploadWindow(weekStart, config);
const weekId = weekStart.toISOString().split('T')[0];
const weekStartStr = formatDateLocal(weekStart);
const isSkipped = skippedWeeks.has(weekStartStr);

const isWithin = isNowWithinWindow(windowStart, windowEnd);
const isValidCurrentWindow = isWithin && !isSkipped;

const window: UploadWindow = {
weekId,
windowStart: windowStart.toISOString(),
windowEnd: windowEnd.toISOString(),
isSkipped,
Expand Down
1 change: 1 addition & 0 deletions spec/src/lib/cheatsheet-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface CheatSheetRequest {
export interface UploadWindow {
windowStart: string; // ISO string
windowEnd: string; // ISO string
weekId: string;
isSkipped: boolean;
isWithinWindow: boolean;
}
Expand Down