File size: 6,132 Bytes
a7ffb22 0815e01 a7ffb22 b6f5bc4 a7ffb22 0815e01 a7ffb22 0815e01 a7ffb22 0815e01 a7ffb22 72ba2ce a7ffb22 0815e01 b6f5bc4 0815e01 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 b1beb2e b6f5bc4 a7ffb22 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | // The path: a trail of lessons. Each lesson is a hub with its skills as numbered stars in a ring around it; the owl perches on the
// suggested next skill. Every star is a link to its exercise screen (#/skill/<id>) and says its state in a symbol and a word.
import { api, swr } from "./api.js";
import { esc, fmt } from "./math.js";
import { owl, ringSvg, spark } from "./sky.js";
import { backToSubjects, forSubject, goalBar, shortDate, tabs } from "./ui.js";
export const STATES = {
not_started: { symbol: "โ", word: "ืื ืืชืืืช" },
learning: { symbol: "โ", word: "ืืืืืื" },
mastered: { symbol: "โ", word: "ื ืฉืืื" },
};
const NEXT = "ืืืฉืืืื ืืื";
const FADING = "ืืืขื";
const plain = text => String(text ?? "").replace(/\$/g, "");
function star(skill, i, n, isNext) {
const t = (-90 + 360 * i / n) * Math.PI / 180, base = STATES[skill.state] || STATES.not_started;
const symbol = skill.faded ? `${base.symbol} โ` : base.symbol, word = skill.faded ? `${base.word}, ${FADING}` : base.word;
return `<a class="star st-${esc(skill.state)}${skill.faded ? " faded" : ""}${isNext ? " next" : ""}" href="#/skill/${esc(skill.id)}" data-skill="${esc(skill.id)}"
style="--dx:${Math.cos(t).toFixed(3)};--dy:${Math.sin(t).toFixed(3)}"
aria-label="${i + 1}. ${esc(plain(skill.name))}: ${word}${isNext ? ", " + NEXT : ""}">
<span class="node">${spark()}<span class="num" aria-hidden="true">${i + 1}</span>${isNext ? `<span class="perch">${owl(30)}</span>` : ""}</span>
<span class="lbl">${fmt(skill.short)}<em><span aria-hidden="true">${symbol}</span> ${word}${isNext ? " ยท " + NEXT : ""}</em></span></a>`;
}
/** The lesson's quiz, a node on the trail after its ring: always open, its state in words. */
function quizNode(l) {
const { best, open } = l.quiz || {};
const state = [best !== null && best !== undefined ? `โ ืืฆืืื: ${best}` : "โ ืขืื ืื ื ืขืฉื", open ? "โ ืืืื ืคืชืื" : ""].filter(Boolean).join(" ยท ");
return `<a class="quiz-node${best !== null && best !== undefined ? " graded" : ""}${open ? " open" : ""}" href="#/quiz/${esc(l.id)}" data-quiz="${esc(l.id)}"
aria-label="ืืืื ${esc(plain(l.name))}: ${state}"><span class="qz-icon" aria-hidden="true">${spark()}</span><span><b>ืืืื</b><em>${state}</em></span></a>`;
}
/** "ืืืฉืื", or, for a lesson whose date has passed without it being completed, the marker with the date in words. */
function lessonStatus(l) {
if (l.completed) return `<p class="lesson-status done" data-status="completed"><span aria-hidden="true">โ</span> ืืืฉืื${l.passed ? " (ืขืืจืช ืืช ืืืืื)" : ""}</p>`;
if (l.behind) return `<p class="lesson-status late" data-status="behind"><span aria-hidden="true">โ </span> ืืื ืืืืจ ืืืกืชืืื ืึพ${shortDate(l.due_date)}</p>`;
return "";
}
const behindNote = n => n ? `<p class="behind-note" id="behind-note" role="status"><span aria-hidden="true">โ </span> ${n === 1 ? "ืฉืืขืืจ ืืื ืืคืืืืจ" : `${n} ืฉืืขืืจืื ืืคืืืืจ`} ืืคื ืืชืื ืืช</p>` : "";
function lesson(l, next) {
const n = l.skills.length;
return `<section class="orbit${l.completed ? " completed" : ""}${l.behind ? " behind" : ""}" id="lesson-${esc(l.id)}" aria-label="${esc(plain(l.name))}">
<div class="stage">${ringSvg(l.skills.map(s => s.state))}
<div class="hub"><h3>${fmt(l.name)}</h3><span class="count">${l.mastered} ืืชืื ${n} ื ืฉืืื${l.completed ? " โ" : ""}</span></div>
${l.skills.map((s, i) => star(s, i, n, s.id === next)).join("")}</div>${lessonStatus(l)}</section>${quizNode(l)}`;
}
export function pathHtml(path) {
return `${tabs("path")}${backToSubjects()}
<section class="card path-top" id="path"><h2>ืืขืืงื: ืืืกืืื ืืืื</h2>
<p class="muted">ืืื ืืชืจืืืื ืื ืืืืื ืืช ืืื, ืืขืืืง. ืืช ืืฉืืขืืจืื ืืฉืืงืื ืืืคื ืฉืืืืช.</p>${goalBar(path.today, path.streak)}${behindNote(path.behind)}</section>
<div class="trail">
<div class="mark start">${owl(44)}<b>ืืชืืื</b></div>
${path.lessons.map(l => lesson(l, path.next)).join("")}
<div class="mark end"><span aria-hidden="true" style="font-size:26px">โฆ</span><b>ืกืืฃ ืืืกืืื</b></div></div>`;
}
/** Draw the path. `scroll`: bring the next skill to the middle of the screen (when the path opens); a refresh keeps the place. */
export const show = (view, { scroll = true } = {}) => {
let first = true; // a refresh of the same screen keeps the place
return swr(forSubject("/api/path"), path => { const result = drawPath(view, path, scroll && first); first = false; return result; });
};
function drawPath(view, path, scroll) {
const place = window.scrollY;
view(pathHtml(path));
if (scroll) document.querySelector(".star.next")?.scrollIntoView({ block: "center" });
else window.scrollTo(0, place);
return path;
}
/** Math's card keeps the course's full name; other subjects show their own name. */
const cardName = s => s.id === "math" ? "ืืชืืืืงื ืืืชื ื - ืืฆืืื ืืช" : s.name;
/** The first screen: a card per subject of the student's classes, math first; each bar counts that subject's mastered skills. */
export async function showSubjects(view) {
const subjects = await api("/api/my-subjects");
view(`<section class="subjects" aria-label="ืืงืฆืืขืืช">
<h2>ืื ื ืชืจืื ืืืื?</h2>
${subjects.map(s => `<a class="subject-card" href="#/subject/${encodeURIComponent(s.id)}" data-subject="${esc(s.id)}"><span class="subject-icon" aria-hidden="true">${esc(s.icon)}</span>
<span class="grow"><b>${esc(cardName(s))}</b>
<span class="bar" role="progressbar" aria-label="ืืืืื ืืืืช ืฉืืืฉืืื" aria-valuemin="0" aria-valuemax="${s.total}" aria-valuenow="${s.mastered}"><i style="width:${s.total ? Math.round(100 * s.mastered / s.total) : 0}%"></i></span>
<small>${s.mastered} ืืชืื ${s.total} ืืืืื ืืืืช</small></span><i aria-hidden="true">โน</i></a>`).join("")}</section>`);
return subjects;
}
|