feat: add TypeScript lessons and learning panel
- Introduced a new script to check TypeScript lesson files for errors. - Created a main TypeScript file to render lessons and their details. - Added lesson definitions with starter and answer codes. - Implemented a user interface for navigating and running lessons. - Styled the application with CSS for a better user experience. - Updated README to reflect the new TypeScript section and usage instructions.
This commit is contained in:
151
06-typescript/src/main.ts
Normal file
151
06-typescript/src/main.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import "./style.css";
|
||||
import { lessons } from "./lessons";
|
||||
|
||||
function getRequiredElement<T extends Element>(
|
||||
selector: string,
|
||||
parent: ParentNode = document,
|
||||
): T {
|
||||
const element = parent.querySelector<T>(selector);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`Required element was not found: ${selector}`);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
const app = getRequiredElement<HTMLDivElement>("#app");
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<p class="eyebrow">Vite + TypeScript</p>
|
||||
<h1>TypeScript Learning Lab</h1>
|
||||
<p class="intro">
|
||||
保留原来的 8 个练习目录,同时给这一章补上一个可直接运行的学习面板。
|
||||
</p>
|
||||
</div>
|
||||
<nav class="lesson-nav" aria-label="TypeScript lessons"></nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<section class="hero card">
|
||||
<p class="badge">06-typescript</p>
|
||||
<h2 id="lesson-title"></h2>
|
||||
<p id="lesson-focus" class="focus"></p>
|
||||
<p id="lesson-summary" class="summary"></p>
|
||||
</section>
|
||||
<section class="grid">
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<h3>知识点</h3>
|
||||
</div>
|
||||
<ul id="lesson-points" class="point-list"></ul>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<h3>运行结果</h3>
|
||||
<button id="run-demo" class="run-button" type="button">运行演示</button>
|
||||
</div>
|
||||
<div id="lesson-output" class="output-panel"></div>
|
||||
</article>
|
||||
</section>
|
||||
<section class="grid code-grid">
|
||||
<article class="card code-card">
|
||||
<div class="card-head">
|
||||
<h3>Starter</h3>
|
||||
</div>
|
||||
<pre><code id="starter-code"></code></pre>
|
||||
</article>
|
||||
<article class="card code-card">
|
||||
<div class="card-head">
|
||||
<h3>Answer</h3>
|
||||
</div>
|
||||
<pre><code id="answer-code"></code></pre>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const nav = getRequiredElement<HTMLElement>(".lesson-nav");
|
||||
const title = getRequiredElement<HTMLElement>("#lesson-title");
|
||||
const focus = getRequiredElement<HTMLElement>("#lesson-focus");
|
||||
const summary = getRequiredElement<HTMLElement>("#lesson-summary");
|
||||
const pointList = getRequiredElement<HTMLElement>("#lesson-points");
|
||||
const output = getRequiredElement<HTMLElement>("#lesson-output");
|
||||
const starterCode = getRequiredElement<HTMLElement>("#starter-code");
|
||||
const answerCode = getRequiredElement<HTMLElement>("#answer-code");
|
||||
const runButton = getRequiredElement<HTMLButtonElement>("#run-demo");
|
||||
|
||||
let activeLessonId = lessons[0]?.id ?? "";
|
||||
|
||||
function renderLessonList(): void {
|
||||
nav.innerHTML = lessons
|
||||
.map((lesson) => {
|
||||
const isActive = lesson.id === activeLessonId;
|
||||
return `
|
||||
<button
|
||||
class="lesson-link ${isActive ? "is-active" : ""}"
|
||||
type="button"
|
||||
data-id="${lesson.id}"
|
||||
>
|
||||
<span class="lesson-index">${lesson.id}</span>
|
||||
<span class="lesson-meta">
|
||||
<strong>${lesson.title}</strong>
|
||||
<small>${lesson.focus}</small>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderOutput(lines: string[]): void {
|
||||
output.innerHTML = lines
|
||||
.map((line) => `<div class="output-line">${line}</div>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderActiveLesson(): void {
|
||||
const lesson = lessons.find((item) => item.id === activeLessonId);
|
||||
|
||||
if (!lesson) {
|
||||
return;
|
||||
}
|
||||
|
||||
title.textContent = `${lesson.id}. ${lesson.title}`;
|
||||
focus.textContent = lesson.focus;
|
||||
summary.textContent = lesson.summary;
|
||||
pointList.innerHTML = lesson.keyPoints
|
||||
.map((item) => `<li>${item}</li>`)
|
||||
.join("");
|
||||
starterCode.textContent = lesson.starterCode;
|
||||
answerCode.textContent = lesson.answerCode;
|
||||
renderOutput(lesson.runDemo());
|
||||
renderLessonList();
|
||||
}
|
||||
|
||||
nav.addEventListener("click", (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest<HTMLButtonElement>("[data-id]");
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeLessonId = button.dataset.id ?? activeLessonId;
|
||||
renderActiveLesson();
|
||||
});
|
||||
|
||||
runButton.addEventListener("click", () => {
|
||||
const lesson = lessons.find((item) => item.id === activeLessonId);
|
||||
|
||||
if (!lesson) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderOutput(lesson.runDemo());
|
||||
});
|
||||
|
||||
renderActiveLesson();
|
||||
Reference in New Issue
Block a user