LMS — Classroom Player
A focused e-learning classroom built in vanilla JS: a video player area with poster, scrubber, speed and caption controls, plus a sticky curriculum sidebar that highlights the current lesson and tracks check-offs. Below sit tabbed panels for an interactive click-to-seek transcript, autosaving notes with timestamp insertion, downloadable resources, and a class Q&A thread. Switching lessons updates progress rings and bars, and mark-complete advances to the next item.
MCP
Code
:root {
--brand: #5b5bd6;
--brand-d: #4444c2;
--brand-50: #eeeefc;
--accent: #13b981;
--amber: #f59e0b;
--ink: #1a1a2e;
--ink-2: #44465f;
--muted: #6b6e87;
--bg: #f7f7fb;
--surface: #ffffff;
--line: rgba(26, 26, 46, 0.1);
--ok: #13b981;
--danger: #e05656;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(26, 26, 46, 0.05), 0 2px 8px rgba(26, 26, 46, 0.04);
--sh-2: 0 6px 24px rgba(26, 26, 46, 0.1);
}
[data-theme="study"] {
--ink: #eef0ff;
--ink-2: #c5c8e6;
--muted: #9295b8;
--bg: #14141f;
--surface: #1d1d2c;
--line: rgba(255, 255, 255, 0.1);
--brand-50: #2a2a44;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background .25s, color .25s;
}
h1, h2 { margin: 0; line-height: 1.25; }
.skip {
position: absolute;
left: -999px;
top: 8px;
background: var(--brand);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip:focus { left: 12px; }
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 22px;
background: var(--surface);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 40px; height: 40px;
display: grid; place-items: center;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
border-radius: var(--r-md);
font-size: 20px;
box-shadow: var(--sh-1);
}
.brand-text { display: flex; flex-direction: column; }
.brand-text strong { font-size: 14px; font-weight: 700; }
.course-title { font-size: 12px; color: var(--muted); }
.topbar-progress { display: flex; align-items: center; gap: 14px; }
.ring { position: relative; width: 44px; height: 44px; }
.ring svg { width: 44px; height: 44px; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--brand-50); stroke-width: 5; }
.ring-fg {
fill: none; stroke: var(--accent); stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 113;
stroke-dashoffset: calc(113 - (113 * var(--p)) / 100);
transition: stroke-dashoffset .5s ease;
}
.ring-num {
position: absolute; inset: 0;
display: grid; place-items: center;
font-size: 11px; font-weight: 700;
}
.topbar-meta { display: flex; flex-direction: column; }
.topbar-meta span { font-size: 13px; font-weight: 600; }
.topbar-meta small { font-size: 11px; color: var(--muted); }
.theme-toggle {
display: inline-flex; align-items: center; gap: 7px;
margin-left: 6px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
font: inherit; font-size: 12px; font-weight: 600;
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
transition: border-color .2s, color .2s;
}
.theme-toggle .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); transition: background .2s; }
.theme-toggle[aria-pressed="true"] { border-color: var(--brand); color: var(--brand); }
.theme-toggle[aria-pressed="true"] .dot { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
/* Layout */
.layout {
max-width: 1240px;
margin: 0 auto;
padding: 22px;
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
gap: 22px;
align-items: start;
}
/* Player */
.player {
background: #0e0e1a;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-2);
}
.poster {
position: relative;
aspect-ratio: 16 / 9;
display: grid;
place-items: center;
overflow: hidden;
}
.poster-art { position: absolute; inset: 0; }
.poster-grid {
position: absolute; inset: 0;
background:
linear-gradient(135deg, #1d1d3a, #0e0e1a 60%),
repeating-linear-gradient(0deg, rgba(255,255,255,.04) 0 1px, transparent 1px 38px),
repeating-linear-gradient(90deg, rgba(255,255,255,.04) 0 1px, transparent 1px 38px);
}
.poster-glow {
position: absolute; width: 60%; height: 60%;
top: -10%; right: -10%;
background: radial-gradient(circle, rgba(91,91,214,.55), transparent 70%);
filter: blur(10px);
}
.poster.playing .poster-glow { animation: drift 6s ease-in-out infinite; }
@keyframes drift { 50% { transform: translate(-12%, 18%) scale(1.15); } }
.poster-info {
position: absolute;
left: 22px; bottom: 22px; right: 22px;
color: #fff;
z-index: 2;
}
.now-pill {
display: inline-block;
background: rgba(255,255,255,.14);
backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,.18);
padding: 4px 10px;
border-radius: 999px;
font-size: 11px; font-weight: 600;
margin-bottom: 8px;
}
.poster-info h1 { font-size: clamp(18px, 3vw, 26px); font-weight: 800; }
.poster-info p { margin: 6px 0 0; font-size: 13px; color: rgba(255,255,255,.72); }
.play-btn {
position: absolute; z-index: 3;
width: 76px; height: 76px;
border-radius: 50%;
border: none;
background: rgba(255,255,255,.94);
color: var(--brand-d);
display: grid; place-items: center;
cursor: pointer;
box-shadow: 0 8px 30px rgba(0,0,0,.4);
transition: transform .2s, opacity .2s;
}
.play-btn svg { width: 34px; height: 34px; fill: currentColor; margin-left: 4px; }
.play-btn:hover { transform: scale(1.07); }
.poster.playing .play-btn { opacity: 0; pointer-events: none; transform: scale(.6); }
/* Controls */
.controls {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px;
background: #14141f;
border-top: 1px solid rgba(255,255,255,.06);
}
.ctrl {
border: none; background: transparent; cursor: pointer;
color: rgba(255,255,255,.85);
font: inherit; font-weight: 600; font-size: 13px;
display: grid; place-items: center;
border-radius: var(--r-sm);
padding: 6px;
transition: background .15s, color .15s;
}
.ctrl:hover { background: rgba(255,255,255,.08); color: #fff; }
.ctrl svg { width: 20px; height: 20px; fill: currentColor; }
.ctrl-play { background: var(--brand); width: 36px; height: 36px; }
.ctrl-play:hover { background: var(--brand-d); }
.ic-pause { display: none; }
.player.playing .ic-play { display: none; }
.player.playing .ic-pause { display: block; }
.rate { min-width: 34px; }
.ccBtn { }
.ctrl[aria-pressed="false"] { color: rgba(255,255,255,.4); }
.time { font-size: 12px; color: rgba(255,255,255,.6); font-variant-numeric: tabular-nums; min-width: 38px; text-align: center; }
.scrub {
flex: 1;
position: relative;
height: 6px;
background: rgba(255,255,255,.14);
border-radius: 999px;
cursor: pointer;
}
.scrub:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; }
.scrub-buf { position: absolute; inset: 0; width: 62%; background: rgba(255,255,255,.12); border-radius: 999px; }
.scrub-fill { position: absolute; inset: 0; width: 0; background: linear-gradient(90deg, var(--brand), #7d7dff); border-radius: 999px; }
.scrub-knob {
position: absolute; top: 50%; left: 0;
width: 14px; height: 14px;
background: #fff;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2px 6px rgba(0,0,0,.5);
transition: transform .12s;
}
.scrub:hover .scrub-knob { transform: translate(-50%, -50%) scale(1.2); }
/* Lesson bar */
.lesson-bar {
display: flex; align-items: center; justify-content: space-between;
gap: 14px;
margin-top: 16px;
padding: 14px 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-1);
flex-wrap: wrap;
}
.lesson-bar-info { display: flex; align-items: center; gap: 12px; min-width: 0; }
.lesson-bar-title { font-weight: 700; font-size: 15px; }
.lesson-bar-actions { display: flex; gap: 8px; }
.pill {
display: inline-block;
font-size: 11px; font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
white-space: nowrap;
}
.pill.level { background: var(--brand-50); color: var(--brand-d); }
.pill.beginner { background: rgba(19,185,129,.14); color: #0c8a5f; }
.pill.advanced { background: rgba(245,158,11,.16); color: #b27407; }
.btn {
font: inherit; font-weight: 600; font-size: 13px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
padding: 9px 15px;
cursor: pointer;
transition: background .15s, border-color .15s, transform .1s, opacity .15s;
}
.btn:hover { border-color: var(--brand); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn.primary:hover { background: var(--brand-d); border-color: var(--brand-d); }
.btn.ghost { background: transparent; }
.btn.small { padding: 7px 12px; font-size: 12px; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
/* Tabs */
.tabs {
display: flex; gap: 4px;
margin-top: 18px;
border-bottom: 1px solid var(--line);
overflow-x: auto;
}
.tab {
border: none; background: transparent;
font: inherit; font-weight: 600; font-size: 14px;
color: var(--muted);
padding: 10px 14px;
cursor: pointer;
border-bottom: 2px solid transparent;
white-space: nowrap;
display: inline-flex; align-items: center; gap: 6px;
transition: color .15s, border-color .15s;
}
.tab:hover { color: var(--ink-2); }
.tab.is-active { color: var(--brand); border-bottom-color: var(--brand); }
.badge {
background: var(--brand-50); color: var(--brand-d);
font-size: 11px; font-weight: 700;
padding: 1px 7px; border-radius: 999px;
}
.panels { padding: 18px 2px; }
.panel { animation: fade .25s ease; }
.panel[hidden] { display: none; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } }
.panel-hint { margin: 0 0 12px; font-size: 13px; color: var(--muted); }
/* Transcript */
.transcript { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.transcript li {
display: flex; gap: 12px;
padding: 9px 12px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background .15s;
}
.transcript li:hover { background: var(--brand-50); }
.transcript li.active { background: var(--brand-50); }
.transcript li.active .tr-text { color: var(--ink); font-weight: 500; }
.tr-time {
font-size: 12px; font-weight: 700; color: var(--brand);
font-variant-numeric: tabular-nums;
min-width: 42px;
}
.tr-text { font-size: 14px; color: var(--ink-2); }
/* Notes */
.notes-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.notes-head h2 { font-size: 15px; }
.notes-stamp { font-size: 12px; color: var(--muted); }
.notes-area {
width: 100%;
min-height: 150px;
resize: vertical;
font: inherit; font-size: 14px;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
}
.notes-area:focus { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand); }
.notes-actions { display: flex; gap: 8px; margin-top: 12px; }
/* Resources */
.res-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.res-list li {
display: flex; align-items: center; gap: 14px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
}
.res-list li div { flex: 1; min-width: 0; }
.res-list strong { display: block; font-size: 14px; }
.res-list small { color: var(--muted); font-size: 12px; }
.res-ic {
width: 42px; height: 42px;
display: grid; place-items: center;
border-radius: var(--r-sm);
font-size: 11px; font-weight: 800; color: #fff;
}
.res-ic.pdf { background: #e05656; }
.res-ic.zip { background: var(--amber); }
.res-ic.link { background: var(--brand); }
/* Q&A */
.qa-form { display: flex; gap: 8px; margin-bottom: 16px; }
.qa-input {
flex: 1;
font: inherit; font-size: 14px;
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
}
.qa-input:focus { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand); }
.qa-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.qa-list li { display: flex; gap: 12px; }
.qa-av {
width: 38px; height: 38px; flex-shrink: 0;
border-radius: 50%;
background: var(--c, var(--brand));
color: #fff;
display: grid; place-items: center;
font-size: 13px; font-weight: 700;
}
.qa-meta { margin: 0; font-size: 13px; color: var(--muted); }
.qa-meta strong { color: var(--ink); }
.qa-body p { margin: 2px 0; font-size: 14px; }
.qa-body code { background: var(--brand-50); color: var(--brand-d); padding: 1px 5px; border-radius: 5px; font-size: 13px; }
.qa-foot { font-size: 12px; color: var(--muted); margin-top: 4px; }
.qa-up { border: none; background: none; color: var(--brand); font: inherit; font-size: 12px; font-weight: 700; cursor: pointer; padding: 0; }
.qa-up:hover { text-decoration: underline; }
/* Curriculum sidebar */
.curriculum {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
position: sticky;
top: 84px;
overflow: hidden;
}
.cur-head { padding: 18px 18px 12px; display: flex; align-items: baseline; justify-content: space-between; }
.cur-head h2 { font-size: 15px; }
.cur-head span { font-size: 12px; color: var(--muted); }
.cur-progress { display: flex; align-items: center; gap: 10px; padding: 0 18px 14px; }
.cur-progress .bar { flex: 1; height: 7px; background: var(--brand-50); border-radius: 999px; overflow: hidden; }
.cur-progress .bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), #34d399); border-radius: 999px; transition: width .5s ease; }
.cur-progress span { font-size: 12px; font-weight: 600; color: var(--accent); white-space: nowrap; }
.modules { max-height: 540px; overflow-y: auto; border-top: 1px solid var(--line); }
.module-h {
padding: 12px 18px 6px;
font-size: 11px; font-weight: 700;
letter-spacing: .04em; text-transform: uppercase;
color: var(--muted);
}
.lesson {
display: flex; align-items: center; gap: 12px;
width: 100%;
text-align: left;
border: none;
background: transparent;
padding: 10px 18px;
cursor: pointer;
font: inherit;
border-left: 3px solid transparent;
transition: background .15s;
}
.lesson:hover { background: var(--brand-50); }
.lesson.current { background: var(--brand-50); border-left-color: var(--brand); }
.lesson:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.check {
width: 22px; height: 22px; flex-shrink: 0;
border-radius: 50%;
border: 2px solid var(--line);
display: grid; place-items: center;
font-size: 12px; color: transparent;
transition: background .2s, border-color .2s, color .2s;
}
.lesson.done .check { background: var(--accent); border-color: var(--accent); color: #fff; }
.lesson.current:not(.done) .check { border-color: var(--brand); }
.lesson-meta { flex: 1; min-width: 0; }
.lesson-meta .lt { display: block; font-size: 14px; font-weight: 600; color: var(--ink); }
.lesson.done .lesson-meta .lt { color: var(--muted); }
.lesson-meta .ld { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 7px; }
.dur-tag { font-variant-numeric: tabular-nums; }
.kind-tag { font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 5px; background: var(--bg); color: var(--ink-2); border: 1px solid var(--line); }
.kind-tag.quiz { background: rgba(245,158,11,.14); color: #b27407; border-color: transparent; }
/* Toast */
.toast {
position: fixed;
left: 50%; bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: 999px;
font-size: 13px; font-weight: 600;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
z-index: 60;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.curriculum { position: static; order: 2; }
}
@media (max-width: 520px) {
.layout { padding: 14px; gap: 14px; }
.topbar { padding: 12px 14px; }
.topbar-meta { display: none; }
.lesson-bar-actions { width: 100%; }
.lesson-bar-actions .btn { flex: 1; }
.completeBtn { flex: 1; }
.poster-info { left: 14px; right: 14px; bottom: 14px; }
.controls { gap: 8px; padding: 10px 12px; }
.rate, #ccBtn { display: none; }
}(function () {
"use strict";
// ---- Course data (fictional) ----
var COURSE = {
title: "Modern CSS Layout Mastery",
modules: [
{
name: "Module 1 · Foundations",
lessons: [
{ id: "l1", t: "The box model, really", d: "08:12", level: "beginner", kind: "video" },
{ id: "l2", t: "Display & document flow", d: "10:05", level: "beginner", kind: "video" }
]
},
{
name: "Module 2 · Flexbox",
lessons: [
{ id: "l3", t: "Aligning items with flex utilities", d: "12:40", level: "intermediate", kind: "video" },
{ id: "l4", t: "Wrapping, gap & grow/shrink", d: "11:18", level: "intermediate", kind: "video" },
{ id: "l5", t: "Checkpoint: flex quiz", d: "5 questions", level: "intermediate", kind: "quiz" }
]
},
{
name: "Module 3 · Grid",
lessons: [
{ id: "l6", t: "Grid template areas", d: "13:50", level: "advanced", kind: "video" },
{ id: "l7", t: "Auto-fit, minmax & responsive grids", d: "14:22", level: "advanced", kind: "video" },
{ id: "l8", t: "Final project: dashboard layout", d: "09:01", level: "advanced", kind: "video" }
]
}
],
transcripts: {
l3: [
{ t: 0, s: "0:00", x: "Welcome back. In this lesson we tackle alignment in Flexbox." },
{ t: 28, s: "0:28", x: "Remember: a flex container has a main axis and a cross axis." },
{ t: 65, s: "1:05", x: "justify-content controls spacing along the main axis." },
{ t: 112, s: "1:52", x: "align-items, by contrast, positions children on the cross axis." },
{ t: 168, s: "2:48", x: "Let's open the demo and toggle each value live." },
{ t: 240, s: "4:00", x: "Notice how stretch is the default for align-items." },
{ t: 322, s: "5:22", x: "Use align-self to override alignment for a single child." },
{ t: 410, s: "6:50", x: "Quick recap, then your turn in the exercise files." }
]
}
};
var DEFAULT_TRANSCRIPT = [
{ t: 0, s: "0:00", x: "Transcript for this lesson is being generated." },
{ t: 30, s: "0:30", x: "Switch to a Flexbox lesson to see the full interactive transcript." }
];
// Flatten lessons preserving module reference
var FLAT = [];
COURSE.modules.forEach(function (m) {
m.lessons.forEach(function (l) { FLAT.push({ lesson: l, module: m.name }); });
});
var state = {
current: 2, // index into FLAT (l3)
done: {}, // id -> true
playing: false,
pos: 0, // seconds
dur: 760,
rate: 1,
timer: null
};
var RATES = [1, 1.25, 1.5, 2, 0.75];
// ---- DOM helpers ----
var $ = function (s) { return document.querySelector(s); };
function toast(msg) {
var el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toast._t);
toast._t = setTimeout(function () { el.classList.remove("show"); }, 2200);
}
function fmt(sec) {
sec = Math.max(0, Math.floor(sec));
var m = Math.floor(sec / 60), s = sec % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function durToSec(d) {
if (!/:/.test(d)) return 300;
var p = d.split(":");
return parseInt(p[0], 10) * 60 + parseInt(p[1], 10);
}
// ---- Render curriculum ----
function renderCurriculum() {
var wrap = $("#modules");
wrap.innerHTML = "";
var idx = 0;
COURSE.modules.forEach(function (m) {
var h = document.createElement("div");
h.className = "module-h";
h.textContent = m.name;
wrap.appendChild(h);
m.lessons.forEach(function (l) {
var fi = idx;
var btn = document.createElement("button");
btn.className = "lesson";
btn.type = "button";
if (fi === state.current) btn.classList.add("current");
if (state.done[l.id]) btn.classList.add("done");
btn.setAttribute("aria-current", fi === state.current ? "true" : "false");
btn.innerHTML =
'<span class="check" aria-hidden="true">✓</span>' +
'<span class="lesson-meta">' +
'<span class="lt">' + l.t + '</span>' +
'<span class="ld"><span class="dur-tag">' + l.d + '</span>' +
'<span class="kind-tag ' + (l.kind === "quiz" ? "quiz" : "") + '">' +
(l.kind === "quiz" ? "QUIZ" : "VIDEO") + '</span></span>' +
'</span>';
btn.addEventListener("click", function () { goTo(fi); });
wrap.appendChild(btn);
idx++;
});
});
}
// ---- Progress ----
function progress() {
var total = FLAT.length;
var done = Object.keys(state.done).length;
var pct = Math.round((done / total) * 100);
$("#ringFg").style.setProperty;
$("#courseRing").style.setProperty("--p", pct);
$("#ringNum").textContent = pct + "%";
$("#progressLabel").textContent = done + " of " + total + " lessons";
$("#curBar").style.width = pct + "%";
$("#curPct").textContent = pct + "% complete";
return pct;
}
// ---- Load a lesson ----
function loadLesson() {
var entry = FLAT[state.current];
var l = entry.lesson;
$("#nowModule").textContent = entry.module;
$("#nowTitle").textContent = l.t;
state.dur = durToSec(l.d);
var totalIdx = "Lesson " + (state.current + 1) + " of " + FLAT.length;
$("#nowDur").textContent = (/:/.test(l.d) ? l.d + " · " : "") + totalIdx;
$("#barTitle").textContent = l.t;
var lvl = $("#barLevel");
lvl.textContent = l.level.charAt(0).toUpperCase() + l.level.slice(1);
lvl.className = "pill level " + l.level;
$("#durTime").textContent = fmt(state.dur);
// complete button label
var cb = $("#completeBtn");
if (state.done[l.id]) {
cb.textContent = state.current < FLAT.length - 1 ? "Completed ✓ · Next lesson" : "Completed ✓";
} else {
cb.textContent = state.current < FLAT.length - 1 ? "Mark complete & next" : "Mark complete";
}
$("#prevBtn").disabled = state.current === 0;
// reset playback
pause();
state.pos = 0;
updateScrub();
renderTranscript();
highlightCurrent();
}
function highlightCurrent() {
var btns = document.querySelectorAll(".lesson");
btns.forEach(function (b, i) {
b.classList.toggle("current", i === state.current);
b.setAttribute("aria-current", i === state.current ? "true" : "false");
});
}
function goTo(i) {
if (i < 0 || i >= FLAT.length) return;
state.current = i;
loadLesson();
$("#player").scrollIntoView({ behavior: "smooth", block: "nearest" });
toast("Now playing: " + FLAT[i].lesson.t);
}
// ---- Transcript ----
function renderTranscript() {
var l = FLAT[state.current].lesson;
var lines = COURSE.transcripts[l.id] || DEFAULT_TRANSCRIPT;
var ul = $("#transcript");
ul.innerHTML = "";
lines.forEach(function (ln) {
var li = document.createElement("li");
li.dataset.t = ln.t;
li.innerHTML = '<span class="tr-time">' + ln.s + '</span><span class="tr-text">' + ln.x + '</span>';
li.addEventListener("click", function () {
state.pos = ln.t;
updateScrub();
markActiveLine();
if (!state.playing) play();
toast("Jumped to " + ln.s);
});
ul.appendChild(li);
});
markActiveLine();
}
function markActiveLine() {
var items = document.querySelectorAll("#transcript li");
var active = null;
items.forEach(function (li) {
if (parseFloat(li.dataset.t) <= state.pos) active = li;
li.classList.remove("active");
});
if (active) active.classList.add("active");
}
// ---- Playback ----
function updateScrub() {
var pct = state.dur ? (state.pos / state.dur) * 100 : 0;
pct = Math.min(100, Math.max(0, pct));
$("#scrubFill").style.width = pct + "%";
$("#scrubKnob").style.left = pct + "%";
$("#curTime").textContent = fmt(state.pos);
var sc = $("#scrub");
sc.setAttribute("aria-valuenow", Math.round(pct));
}
function tick() {
state.pos += state.rate;
if (state.pos >= state.dur) {
state.pos = state.dur;
updateScrub();
pause();
toast("Lesson finished");
return;
}
updateScrub();
markActiveLine();
}
function play() {
if (state.playing) return;
state.playing = true;
$("#player").classList.add("playing");
$("#poster").classList.add("playing");
state.timer = setInterval(tick, 1000);
}
function pause() {
state.playing = false;
$("#player").classList.remove("playing");
$("#poster").classList.remove("playing");
if (state.timer) { clearInterval(state.timer); state.timer = null; }
}
function togglePlay() { state.playing ? pause() : play(); }
// ---- Wire controls ----
$("#playBtn").addEventListener("click", play);
$("#ctrlPlay").addEventListener("click", togglePlay);
$("#scrub").addEventListener("click", function (e) {
var r = this.getBoundingClientRect();
state.pos = ((e.clientX - r.left) / r.width) * state.dur;
updateScrub();
markActiveLine();
});
$("#scrub").addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") { state.pos = Math.min(state.dur, state.pos + 5); updateScrub(); markActiveLine(); e.preventDefault(); }
else if (e.key === "ArrowLeft") { state.pos = Math.max(0, state.pos - 5); updateScrub(); markActiveLine(); e.preventDefault(); }
else if (e.key === " " || e.key === "Enter") { togglePlay(); e.preventDefault(); }
});
$("#rateBtn").addEventListener("click", function () {
var i = (RATES.indexOf(state.rate) + 1) % RATES.length;
state.rate = RATES[i];
this.textContent = state.rate + "×";
toast("Speed " + state.rate + "×");
});
$("#ccBtn").addEventListener("click", function () {
var on = this.getAttribute("aria-pressed") === "true";
this.setAttribute("aria-pressed", String(!on));
toast("Captions " + (!on ? "on" : "off"));
});
// ---- Lesson navigation ----
$("#prevBtn").addEventListener("click", function () { goTo(state.current - 1); });
$("#completeBtn").addEventListener("click", function () {
var l = FLAT[state.current].lesson;
var wasDone = state.done[l.id];
if (!wasDone) {
state.done[l.id] = true;
var btn = document.querySelectorAll(".lesson")[state.current];
if (btn) btn.classList.add("done");
var pct = progress();
toast("Lesson complete ✓ " + pct + "% of course done");
}
if (state.current < FLAT.length - 1) {
goTo(state.current + 1);
} else if (!wasDone) {
progress();
loadLesson();
toast("🎉 Course complete! Nice work.");
}
});
// ---- Tabs ----
document.querySelectorAll(".tab").forEach(function (tab) {
tab.addEventListener("click", function () {
document.querySelectorAll(".tab").forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
var name = tab.dataset.tab;
document.querySelectorAll(".panel").forEach(function (p) {
p.hidden = p.dataset.panel !== name;
p.classList.toggle("is-active", p.dataset.panel === name);
});
});
});
// ---- Notes ----
var notesArea = $("#notesArea");
var savedNote = "";
$("#stampBtn").addEventListener("click", function () {
var stamp = "[" + fmt(state.pos) + "] ";
var pos = notesArea.selectionStart || notesArea.value.length;
notesArea.value = notesArea.value.slice(0, pos) + stamp + notesArea.value.slice(pos);
notesArea.focus();
$("#notesStamp").textContent = "Unsaved changes";
});
notesArea.addEventListener("input", function () {
$("#notesStamp").textContent = notesArea.value === savedNote ? "All changes saved" : "Unsaved changes";
});
$("#saveNote").addEventListener("click", function () {
savedNote = notesArea.value;
var now = new Date();
var hh = now.getHours(), mm = now.getMinutes();
var ap = hh >= 12 ? "PM" : "AM";
hh = hh % 12 || 12;
$("#notesStamp").textContent = "Saved at " + hh + ":" + (mm < 10 ? "0" : "") + mm + " " + ap;
toast("Note saved");
});
// ---- Q&A ----
$("#qaForm").addEventListener("submit", function (e) {
e.preventDefault();
var input = $("#qaInput");
var v = input.value.trim();
if (!v) return;
var li = document.createElement("li");
li.innerHTML =
'<div class="qa-av" style="--c:#f59e0b">You</div>' +
'<div class="qa-body">' +
'<p class="qa-meta"><strong>You</strong> · just now</p>' +
'<p></p>' +
'<div class="qa-foot"><button class="qa-up" type="button">▲ 0</button> · 0 replies</div>' +
'</div>';
li.querySelector(".qa-body p:nth-child(2)").textContent = v;
$("#qaList").prepend(li);
input.value = "";
var b = $("#tab-qa").querySelector(".badge");
if (b) b.textContent = String(parseInt(b.textContent, 10) + 1);
toast("Question posted to the class");
});
document.addEventListener("click", function (e) {
var up = e.target.closest(".qa-up");
if (up) {
var n = parseInt(up.textContent.replace(/\D/g, ""), 10) || 0;
up.textContent = "▲ " + (n + 1);
}
});
// ---- Study mode ----
$("#themeToggle").addEventListener("click", function () {
var on = document.documentElement.getAttribute("data-theme") === "study";
if (on) {
document.documentElement.removeAttribute("data-theme");
this.setAttribute("aria-pressed", "false");
toast("Light mode");
} else {
document.documentElement.setAttribute("data-theme", "study");
this.setAttribute("aria-pressed", "true");
toast("Study mode on");
}
});
// ---- Init ----
// pre-complete first two lessons for realistic progress
state.done.l1 = true;
state.done.l2 = true;
renderCurriculum();
progress();
loadLesson();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LMS — Classroom Player</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#player">Skip to player</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◣</span>
<div class="brand-text">
<strong>Stealthis Academy</strong>
<span class="course-title">Modern CSS Layout Mastery</span>
</div>
</div>
<div class="topbar-progress" aria-label="Course progress">
<div class="ring" id="courseRing" style="--p:0">
<svg viewBox="0 0 44 44" aria-hidden="true">
<circle class="ring-bg" cx="22" cy="22" r="18" />
<circle class="ring-fg" cx="22" cy="22" r="18" id="ringFg" />
</svg>
<span class="ring-num" id="ringNum">0%</span>
</div>
<div class="topbar-meta">
<span id="progressLabel">0 of 8 lessons</span>
<small>Course completion</small>
</div>
<button class="theme-toggle" id="themeToggle" aria-pressed="false" title="Toggle study mode">
<span class="dot" aria-hidden="true"></span> Study mode
</button>
</div>
</header>
<main class="layout">
<section class="stage" aria-label="Lesson content">
<div class="player" id="player" tabindex="-1">
<div class="poster" id="poster">
<div class="poster-art" aria-hidden="true">
<span class="poster-grid"></span>
<span class="poster-glow"></span>
</div>
<div class="poster-info">
<span class="now-pill" id="nowModule">Module 2 · Flexbox</span>
<h1 id="nowTitle">Aligning items with flex utilities</h1>
<p id="nowDur">12:40 · Lesson 3 of 8</p>
</div>
<button class="play-btn" id="playBtn" aria-label="Play lesson">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
</button>
</div>
<div class="controls" role="group" aria-label="Player controls">
<button class="ctrl ctrl-play" id="ctrlPlay" aria-label="Play">
<svg class="ic-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
</button>
<span class="time" id="curTime">0:00</span>
<div class="scrub" id="scrub" role="slider" aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" tabindex="0">
<div class="scrub-buf"></div>
<div class="scrub-fill" id="scrubFill"></div>
<div class="scrub-knob" id="scrubKnob"></div>
</div>
<span class="time" id="durTime">12:40</span>
<button class="ctrl rate" id="rateBtn" aria-label="Playback speed">1×</button>
<button class="ctrl" id="ccBtn" aria-pressed="true" aria-label="Captions">CC</button>
</div>
</div>
<div class="lesson-bar">
<div class="lesson-bar-info">
<span class="pill level" id="barLevel">Intermediate</span>
<span class="lesson-bar-title" id="barTitle">Aligning items with flex utilities</span>
</div>
<div class="lesson-bar-actions">
<button class="btn ghost" id="prevBtn">Previous</button>
<button class="btn primary" id="completeBtn">Mark complete & next</button>
</div>
</div>
<div class="tabs" role="tablist" aria-label="Lesson details">
<button class="tab is-active" role="tab" aria-selected="true" data-tab="transcript" id="tab-transcript">Transcript</button>
<button class="tab" role="tab" aria-selected="false" data-tab="notes" id="tab-notes">Notes</button>
<button class="tab" role="tab" aria-selected="false" data-tab="resources" id="tab-resources">Resources</button>
<button class="tab" role="tab" aria-selected="false" data-tab="qa" id="tab-qa">Q&A <span class="badge">5</span></button>
</div>
<div class="panels">
<div class="panel is-active" role="tabpanel" data-panel="transcript" aria-labelledby="tab-transcript">
<p class="panel-hint">Click any line to jump to that moment.</p>
<ul class="transcript" id="transcript"></ul>
</div>
<div class="panel" role="tabpanel" data-panel="notes" aria-labelledby="tab-notes" hidden>
<div class="notes-head">
<h2>Your notes</h2>
<span class="notes-stamp" id="notesStamp">Not saved yet</span>
</div>
<textarea id="notesArea" class="notes-area" placeholder="Jot down a key idea, a timestamp, or a question…" aria-label="Lesson notes"></textarea>
<div class="notes-actions">
<button class="btn small" id="stampBtn" type="button">+ Insert timestamp</button>
<button class="btn primary small" id="saveNote" type="button">Save note</button>
</div>
</div>
<div class="panel" role="tabpanel" data-panel="resources" aria-labelledby="tab-resources" hidden>
<ul class="res-list">
<li><span class="res-ic pdf">PDF</span><div><strong>Flexbox cheat sheet</strong><small>2 pages · 480 KB</small></div><button class="btn small ghost">Download</button></li>
<li><span class="res-ic zip">ZIP</span><div><strong>Starter files — lesson 3</strong><small>HTML + CSS · 1.2 MB</small></div><button class="btn small ghost">Download</button></li>
<li><span class="res-ic link">URL</span><div><strong>MDN: CSS Flexible Box Layout</strong><small>Reference docs</small></div><button class="btn small ghost">Open</button></li>
</ul>
</div>
<div class="panel" role="tabpanel" data-panel="qa" aria-labelledby="tab-qa" hidden>
<form class="qa-form" id="qaForm">
<input id="qaInput" class="qa-input" placeholder="Ask the class a question…" aria-label="Ask a question" />
<button class="btn primary small" type="submit">Post</button>
</form>
<ul class="qa-list" id="qaList">
<li>
<div class="qa-av" style="--c:#5b5bd6">PR</div>
<div class="qa-body">
<p class="qa-meta"><strong>Priya Raman</strong> · 2 days ago</p>
<p>Does <code>align-items</code> affect the main axis or the cross axis?</p>
<div class="qa-foot"><button class="qa-up" type="button">▲ 12</button> · 3 replies</div>
</div>
</li>
<li>
<div class="qa-av" style="--c:#13b981">DM</div>
<div class="qa-body">
<p class="qa-meta"><strong>Diego Morales</strong> · 5 days ago</p>
<p>When should I reach for <code>gap</code> vs. margins between flex children?</p>
<div class="qa-foot"><button class="qa-up" type="button">▲ 8</button> · 1 reply</div>
</div>
</li>
</ul>
</div>
</div>
</section>
<aside class="curriculum" aria-label="Course curriculum">
<div class="cur-head">
<h2>Course content</h2>
<span id="curMeta">8 lessons · 1h 24m</span>
</div>
<div class="cur-progress">
<div class="bar"><i id="curBar"></i></div>
<span id="curPct">0% complete</span>
</div>
<div class="modules" id="modules"></div>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Classroom Player
A self-contained learning classroom for the fictional course Modern CSS Layout Mastery. The stage shows a poster-style player with a play overlay, a draggable scrubber, running time, playback-speed cycling, and a caption toggle. A right-hand curriculum sidebar groups eight lessons into three modules, marks the active lesson with a brand accent, and shows completion check-offs alongside a live progress bar and a course-completion ring in the header.
Everything is wired with vanilla JavaScript. Clicking a lesson loads it and scrolls the player into view; transcript lines are click-to-seek and stay in sync with the simulated playhead; the notes tab autosaves state and can insert the current timestamp; and the Q&A tab accepts new questions and upvotes. Pressing Mark complete records the lesson, recalculates the ring, bar and percentage, then advances to the next item — finishing the last lesson celebrates course completion.
The tabs below the player swap between transcript, notes, resources, and Q&A panels, and an optional study mode flips the interface into a calm dark theme for longer reading sessions. All interactions surface a small toast for feedback.
Illustrative UI only — fictional courses, not a real learning platform.