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:
Rotimi Best
2026-06-22 05:43:48 +01:00
committed by GitHub
parent 3547901de6
commit 81294a47f9
193 changed files with 5477 additions and 805 deletions
@@ -1,5 +1,6 @@
import { resolveTemplateId } from '@cio/certificates';
import { DEFAULT_CERTIFICATE_THEME_ID, type CertificateThemeId } from './constants';
import { t } from '$lib/utils/functions/translations';
/**
* Resolves a stored theme/template id. Maps legacy 6-theme ids onto the new
@@ -12,6 +13,46 @@ export function parseCertificateThemeId(raw: string | null | undefined): Certifi
return resolveTemplateId(raw);
}
export type BlockerData = {
code: string;
params?: Record<string, string | number>;
};
export function formatBlockerMessage(blocker: BlockerData): string {
const p = blocker.params ?? {};
switch (blocker.code) {
case 'CERT_PROGRESS':
return t.get('course.certification.blocker_progress', {
current: Number(p.current ?? 0),
required: Number(p.required ?? 0)
});
case 'CERT_DEADLINE_PASSED':
return t.get('course.certification.blocker_deadline');
case 'CERT_NO_CONTENT':
return t.get('course.certification.blocker_no_content');
case 'CERT_FINAL_EXERCISE_NOT_SUBMITTED':
return t.get('course.certification.blocker_final_not_submitted', {
exerciseTitle: String(p.exerciseTitle ?? '')
});
case 'CERT_FINAL_EXERCISE_PENDING_GRADE':
return t.get('course.certification.blocker_final_pending', {
exerciseTitle: String(p.exerciseTitle ?? '')
});
case 'CERT_FINAL_EXERCISE_SCORE':
return t.get('course.certification.blocker_final_score', {
exerciseTitle: String(p.exerciseTitle ?? ''),
bestPercent: Number(p.bestPercent ?? 0),
requiredPercent: Number(p.requiredPercent ?? 0)
});
case 'CERT_FINAL_EXERCISE_MISCONFIGURED':
return t.get('course.certification.blocker_final_misconfigured', {
exerciseTitle: String(p.exerciseTitle ?? '')
});
default:
return blocker.code;
}
}
/** Normalizes DB/API timestamps to an ISO-8601 UTC string accepted by Zod `.datetime()`. */
export function normalizeCertificateIssuedAt(value: string | null | undefined): string {
if (!value) return new Date().toISOString();
@@ -15,6 +15,7 @@ type CourseSettings = {
isContentGroupingEnabled: boolean;
progressionMode: 'free' | 'sequential';
callout: TCourseCallout | null;
welcomeEmailMessage: string;
};
export const settings = writable<CourseSettings>({
@@ -34,5 +35,6 @@ export const settings = writable<CourseSettings>({
isPublished: false,
isContentGroupingEnabled: true,
progressionMode: 'free',
callout: null
callout: null,
welcomeEmailMessage: ''
});