|
417 | 417 | <li class="rail-layer learn" aria-hidden="true">배우기</li><li class="rail-layer build" aria-hidden="true">만들기</li> |
418 | 418 | </ol>`; |
419 | 419 | } |
| 420 | + /* the library topics a level covers, in order, each with the one or two materials to start from |
| 421 | + (data/levels.json topics + data/topics.json). A material's level follows the library: web book chapters |
| 422 | + 1–16 are Level 1 and the rest Level 2; other materials take their repository's level. */ |
| 423 | + const KIND_NAME = { video: "영상", read: "글", code: "코드", slides: "발표", case: "사례", post: "연재" }; |
| 424 | + const KIND_RANK = { video: 0, read: 1, code: 2, slides: 3, case: 4, post: 5 }; |
| 425 | + function itemLevel(x) { |
| 426 | + if (x.repo === "Causal-Inference-with-Python") { const m = /\/(\d+)-[^/]*\.html$/.exec(x.url || ""); return m && Number(m[1]) < 17 ? "L1" : "L2"; } |
| 427 | + const r = state.repos.get(x.repo); |
| 428 | + return r && r.level ? r.level : null; |
| 429 | + } |
| 430 | + function startWith(t, levelId) { |
| 431 | + const want = lvIndex(levelId); |
| 432 | + const dist = (x) => { const k = lvIndex(itemLevel(x)); return k < 0 ? 9 : Math.abs(k - want); }; |
| 433 | + return [...(t.items || [])].filter((x) => x.kind !== "post") |
| 434 | + .sort((a, b) => dist(a) - dist(b) || (KIND_RANK[a.kind] ?? 9) - (KIND_RANK[b.kind] ?? 9)).slice(0, 2); |
| 435 | + } |
| 436 | + function topicOrder(lv) { |
| 437 | + const all = (state.topicData && state.topicData.topics) || []; |
| 438 | + const ts = (lv.topics || []).map((id) => all.find((t) => t.id === id)).filter(Boolean); |
| 439 | + if (!ts.length) return ""; |
| 440 | + const ev = (topic, target) => `data-event="level_topic" data-event-params='${esc(JSON.stringify({ level: lv.id, topic, target }))}'`; |
| 441 | + const row = (t, k) => `<li class="lt" data-shelf="${esc(t.shelf)}"> |
| 442 | + <span class="lt-n" aria-hidden="true">${k}</span> |
| 443 | + <div class="lt-b"><a class="lt-t" href="library.html#${esc(t.id)}" ${ev(t.id, "library")}>${esc(t.name)}</a> |
| 444 | + <span class="lt-go">${startWith(t, lv.id).map((x) => `<a href="${esc(x.url)}"${/^https?:/.test(x.url) && !/^https:\/\/causalinferencelab\.github\.io\//.test(x.url) ? ' target="_blank" rel="noopener"' : ""} ${ev(t.id, "material")}><i>${esc(KIND_NAME[x.kind] || "자료")}</i>${esc(x.title)}</a>`).join("")} |
| 445 | + <a class="lt-all" href="library.html#${esc(t.id)}" ${ev(t.id, "library_all")}>자료 ${(t.items || []).length}개</a></span></div> |
| 446 | + </li>`; |
| 447 | + const FIRST = 5, head = ts.slice(0, FIRST), rest = ts.slice(FIRST); |
| 448 | + return `<p class="lr-k">배우는 주제 <span>${ts.length}개, 순서대로</span></p> |
| 449 | + <ol class="lt-list">${head.map((t, k) => row(t, k + 1)).join("")}</ol> |
| 450 | + ${rest.length ? `<details class="lt-more" data-topics="${lv.id}"><summary>주제 ${rest.length}개 더</summary><ol class="lt-list" start="${FIRST + 1}">${rest.map((t, k) => row(t, FIRST + k + 1)).join("")}</ol></details>` : ""}`; |
| 451 | + } |
| 452 | + |
420 | 453 | /* one level in full: who it is for, what it covers, what you can do after it, and the order to take it */ |
421 | 454 | function levelPanel(lv, i) { |
422 | 455 | const mine = myLevel(), mi = mine ? lvIndex(mine.level) : -1; |
|
426 | 459 | const extra = items.filter((r) => !inSteps.has(r.repo) && r.kind !== "paid"); |
427 | 460 | const books = items.filter((r) => r.kind === "paid"); |
428 | 461 | const build = (lv.quests || []).length > 0; |
| 462 | + const topics = topicOrder(lv); |
429 | 463 | const crew = [...new Set(items.flatMap((r) => r.builders || []))]; |
430 | 464 | const count = `<span aria-label="${quests.length}개 중 ${done}개 클리어">${done} / ${quests.length}</span>`; |
431 | 465 | const list = build |
432 | 466 | ? `<p class="lr-k">직접 해 보기 ${count}</p><ol class="ls-list">${quests.map((q, k) => questRow(q, k + 1)).join("")}</ol> |
433 | 467 | <p class="lr-k">함께 만드는 레포</p><ul class="lr-repos">${[...steps.map((s) => [s.r, s.note]), ...extra.map((r) => [r, r.format || ""])].map(([r, note]) => `<li data-id="${esc(r.repo)}" data-line="${primaryLine(r)}"><button type="button" data-open="${esc(r.repo)}" title="${esc(note)}"><i aria-hidden="true"></i>${esc(r.title)}${r.status === "running" ? `<span class="ls-live">진행 중</span>` : ""}</button></li>`).join("")}</ul>` |
434 | | - : `<p class="lr-k">이 순서로 ${count}</p><ol class="ls-list">${steps.map((s, k) => stepRow(s.r, k + 1, s.note)).join("")}</ol> |
| 468 | + : `<p class="lr-k">${topics ? "따라갈 자료" : "이 순서로"} ${count}</p><ol class="ls-list">${steps.map((s, k) => stepRow(s.r, k + 1, s.note)).join("")}</ol> |
435 | 469 | ${extra.length ? `<details class="lr-more" data-more="${lv.id}"><summary>골라 보기 ${extra.length}개</summary><ul class="ls-list">${extra.map((r) => stepRow(r, 0, "")).join("")}</ul></details>` : ""}`; |
436 | 470 | const text = books.length ? `<p class="lr-also"><span>함께 보는 교재</span>${books.map((b) => `<button type="button" data-open="${esc(b.repo)}" data-id="${esc(b.repo)}">${esc(b.mapLabel || b.title)}</button>`).join("")}</p>` : ""; |
437 | 471 | const more = (lv.more || []).length ? `<p class="lr-also"><span>더 해 보기</span>${lv.more.map((m) => `<a href="${esc(m.href)}"${/^https?:/.test(m.href) ? ' target="_blank" rel="noopener"' : ""} data-event="click_level_extra" data-event-params='${esc(JSON.stringify({ level: lv.id, target: m.title }))}'>${esc(m.title)}</a>`).join("")}</p>` : ""; |
|
451 | 485 | ${lv.for ? `<p class="lr-for"><b>이런 분께</b>${esc(lv.for)}</p>` : ""} |
452 | 486 | </div> |
453 | 487 | <div class="lr-body"> |
454 | | - ${(lv.learn || []).length ? `<p class="lr-k">배우는 것</p><ul class="lr-learn">${lv.learn.map((t) => `<li>${esc(t)}</li>`).join("")}</ul>` : ""} |
| 488 | + ${(lv.learn || []).length && !topics ? `<p class="lr-k">배우는 것</p><ul class="lr-learn">${lv.learn.map((t) => `<li>${esc(t)}</li>`).join("")}</ul>` : ""} |
455 | 489 | ${lv.outcome ? `<p class="lr-out"><b>끝나면</b>${esc(lv.outcome)}</p>` : ""} |
456 | 490 | ${crew.length ? `<p class="lr-crew"><span>만든 Builder</span>${crew.slice(0, 5).map((n) => `<span class="lr-who">${avatarHTML(n, "sm")}<span>${esc(n)}</span></span>`).join("")}${crew.length > 5 ? `<i>외 ${crew.length - 5}명</i>` : ""}</p>` : ""} |
457 | 491 | </div> |
458 | | - <div class="lr-steps">${list}${text}${more}${goal}</div> |
| 492 | + <div class="lr-steps">${topics}${list}${text}${more}${goal}</div> |
459 | 493 | ${nav} |
460 | 494 | </article>`; |
461 | 495 | } |
|
466 | 500 | state.level = id; |
467 | 501 | const keep = shelf.querySelector(`details[data-more="${id}"]`); |
468 | 502 | const wasOpen = keep && keep.open; |
| 503 | + const keepT = shelf.querySelector(`details[data-topics="${id}"]`), topicsOpen = keepT && keepT.open; |
469 | 504 | shelf.innerHTML = levelPanel(lv, i); |
470 | 505 | if (wasOpen) { const d = shelf.querySelector("details[data-more]"); if (d) d.open = true; } |
| 506 | + if (topicsOpen) { const d = shelf.querySelector("details[data-topics]"); if (d) d.open = true; } |
471 | 507 | if (dir && !reduceMotion) { const a = shelf.firstElementChild; a.classList.add(dir > 0 ? "enter-r" : "enter-l"); } |
472 | 508 | wireAvatars(shelf); |
473 | 509 | renderRail(reveal); renderHud(); renderTestCard(); |
|
1958 | 1994 | renderGrowth(); |
1959 | 1995 | renderContrib(); |
1960 | 1996 | renderShelf(); renderWeek(); watchRail(); revealSections(); |
| 1997 | + getJSON("data/topics.json").then((d) => { state.topicData = d; renderShelf(); }).catch(() => { /* the course shows without its topic order */ }); |
1961 | 1998 | document.addEventListener("hub:stamp", onStamp); |
1962 | 1999 | syncStageStamps(); |
1963 | 2000 | const header = $(".site-header"); |
|
0 commit comments