feat: live class sessions, CE workflow, and docs (#683)
Adds live-class session scheduling on lessons (join link, date/time, timezone) with automatic 24h and 1h reminder emails plus a calendar invite (.ics), a time-change notification with an updated invite, a teacher-authored welcome email and session invite on enrollment, and a "Notify students" quiz nudge. LMS gets an upcoming live-sessions card and Join buttons, plus a reusable live-session card component (3 states) in @cio/ui with a Storybook story. The sidebar content tree now reflects sequential progression locking like the content list, and the course-completion confetti regression is fixed. Org-based emails no longer reference ClassroomIO by name. Docs add live-class, welcome-email, and course-progression guides, expand the certificates guide (design, signatures, corrected labels), move plan-required callouts to the top, flatten how-to-guides to the docs root with redirects, and add screenshots. Includes an add-docs-image skill.
This commit is contained in:
@@ -35,6 +35,7 @@ import { courseAiTutorRouter } from '@api/routes/course/ai-tutor';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
||||
import { cloneCourse } from '@api/services/course/clone';
|
||||
import { sanitizeHtml } from '@cio/core/utils/sanitize-html';
|
||||
import { complianceRouter } from '@api/routes/course/compliance';
|
||||
import { contentRouter } from '@api/routes/course/content';
|
||||
import { courseMemberMiddleware } from '@api/middlewares/course-member';
|
||||
@@ -329,6 +330,13 @@ export const courseRouter = new Hono()
|
||||
const validatedData = c.req.valid('json');
|
||||
const { tagIds, ...courseData } = validatedData;
|
||||
|
||||
if (courseData.metadata?.welcomeEmailMessage) {
|
||||
courseData.metadata = {
|
||||
...courseData.metadata,
|
||||
welcomeEmailMessage: sanitizeHtml(courseData.metadata.welcomeEmailMessage)
|
||||
};
|
||||
}
|
||||
|
||||
const result = await updateCourse(courseId, courseData);
|
||||
|
||||
if (tagIds !== undefined) {
|
||||
|
||||
@@ -29,8 +29,14 @@ import {
|
||||
initVideoRecordingUpload
|
||||
} from '@api/services/exercise/video-recording';
|
||||
|
||||
import {
|
||||
getNotifyCourseExerciseStatusService,
|
||||
notifyCourseExerciseService
|
||||
} from '@api/services/course/notify-exercise';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { courseTeamMemberMiddleware } from '@api/middlewares/course-team-member';
|
||||
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
||||
import { courseMemberMiddleware } from '@api/middlewares/course-member';
|
||||
import { courseMemberOrAutomationKeyMiddleware } from '@api/middlewares/course-member-or-automation-key';
|
||||
@@ -82,6 +88,28 @@ export const exerciseRouter = new Hono()
|
||||
return handleError(c, error, 'Failed to fetch exercise submissions overview');
|
||||
}
|
||||
})
|
||||
.post('/:exerciseId/notify', authMiddleware, courseTeamMemberMiddleware, async (c) => {
|
||||
try {
|
||||
const courseId = c.req.param('courseId')!;
|
||||
const exerciseId = c.req.param('exerciseId')!;
|
||||
|
||||
const result = await notifyCourseExerciseService(courseId, exerciseId);
|
||||
return c.json({ success: true, data: result }, 202);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to notify students');
|
||||
}
|
||||
})
|
||||
.get('/:exerciseId/notify/:jobId', authMiddleware, courseTeamMemberMiddleware, async (c) => {
|
||||
try {
|
||||
const jobId = c.req.param('jobId')!;
|
||||
const pollCount = Number.parseInt(c.req.query('pollCount') ?? '0', 10) || 0;
|
||||
|
||||
const envelope = await getNotifyCourseExerciseStatusService(jobId, pollCount);
|
||||
return c.json({ success: true, data: envelope }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to fetch notification status');
|
||||
}
|
||||
})
|
||||
.get(
|
||||
'/:exerciseId',
|
||||
authOrAutomationKeyMiddleware,
|
||||
|
||||
@@ -38,6 +38,8 @@ import { Hono } from '@api/utils/hono';
|
||||
import { ZLessonDownloadContent } from '@cio/utils/validation/course';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { courseMemberMiddleware } from '@api/middlewares/course-member';
|
||||
import { courseTeamMemberMiddleware } from '@api/middlewares/course-team-member';
|
||||
import { notifyCourseSessionUpdateService } from '@api/services/course/notify-session';
|
||||
import { generateLessonPdf } from '@api/utils/lesson';
|
||||
import { getGroupMemberIdByCourseAndProfile } from '@cio/db/queries/group';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
@@ -121,6 +123,23 @@ export const lessonRouter = new Hono()
|
||||
}
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/:lessonId/notify-session-update',
|
||||
authMiddleware,
|
||||
courseTeamMemberMiddleware,
|
||||
zValidator('param', ZLessonGetParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const courseId = c.req.param('courseId')!;
|
||||
const { lessonId } = c.req.valid('param');
|
||||
|
||||
const result = await notifyCourseSessionUpdateService(courseId, lessonId);
|
||||
return c.json({ success: true, data: result }, 202);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to notify session update');
|
||||
}
|
||||
}
|
||||
)
|
||||
.delete('/:lessonId', authMiddleware, courseMemberMiddleware, zValidator('param', ZLessonGetParam), async (c) => {
|
||||
try {
|
||||
const { lessonId } = c.req.valid('param');
|
||||
|
||||
Reference in New Issue
Block a user