Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function App() {
<aside className={`sidebar ${menuOpen ? "open" : ""}`}>
<button className="brand" type="button" onClick={() => goPath("/map")}>
<span className="brand-mark">SE</span>
<span><b>Software Engineering</b><small>FIELD MANUAL · 2026</small></span>
<span><b>Software Engineering</b></span>
</button>
<div className="nav-label">目錄 / CONTENTS</div>
<nav>
Expand Down Expand Up @@ -118,7 +118,6 @@ export default function App() {
<header className="topbar">
<button className="menu-button" type="button" aria-label="開啟選單" onClick={() => setMenuOpen((value) => !value)}>☰</button>
<div className="breadcrumb"><span>WORKSHOP</span><i>/</i><b>{routeLabel(route)}</b></div>
<div className="top-status">已開放 <b>{progress.coreProgress.ready} / {progress.coreProgress.total}</b></div>
</header>
{route.kind === "map" ? <CurriculumMap curriculum={curriculum} onOpenTrack={(trackId) => goPath(trackPath(trackId))} /> : null}
{route.kind === "track" && activeTrack ? <TrackPage track={activeTrack} completedTopicIds={completedTopicIds} onBackToMap={() => goPath("/map")} onOpenTopic={(topicId) => goTopic(topicId, "lesson")} /> : null}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/AuthLab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ export function AuthLab({ onComplete }: { onComplete: () => void }) {
const progress = configured ? 50 + (demoStep / authDemoSteps.length) * 50 : (questionStep / authQuestions.length) * 50;

return (
<div className="page auth-lab-page">
<div className="page auth-lab-page course-lab-shell">
<header className="lab-header">
<div><p className="kicker">INTERACTIVE LAB / OIDC</p><h1>組出一條<br /><em>可信任的登入路徑</em></h1></div>
<div><h1>組出一條<br /><em>可信任的登入路徑</em></h1></div>
<div className="lab-progress"><span>{demoComplete ? "完成" : configured ? `DEMO ${demoStep + 1} / ${authDemoSteps.length}` : `CONFIG ${questionStep + 1} / ${authQuestions.length}`}</span><div><i style={{ width: `${progress}%` }} /></div></div>
</header>

<div className="auth-lab-grid">
<section className="auth-request">
<p className="kicker">SAMPLE ONLY / NO NETWORK CALL</p>
<dl>
<div><dt>tenant_id</dt><dd>{demoConfig.tenantId}</dd></div>
<div><dt>client_id</dt><dd>{demoConfig.clientId}</dd></div>
Expand All @@ -58,7 +57,6 @@ export function AuthLab({ onComplete }: { onComplete: () => void }) {

<section className="auth-challenge">
{!configured ? <>
<p className="kicker">CONFIGURATION {String(questionStep + 1).padStart(2, "0")}</p>
<h2>{question.prompt}</h2>
<div className="auth-options">{options.map((option) => <button className={selected === option ? "selected" : ""} onClick={() => setSelected(option)} key={option}>{option}</button>)}</div>
{selected && !correct ? <p className="auth-feedback error">欄位不符。</p> : <p className="auth-feedback">{correct ? "正確。" : "選擇一個欄位。"}</p>}
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/components/AuthLesson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ import type { LessonOrientation } from "../topics/types";

export function AuthLesson({ completed, onOpenLab, orientation }: { completed: boolean; onOpenLab: () => void; orientation: LessonOrientation }) {
return (
<div className="page auth-page">
<div className="page auth-page course-lesson-page">
<header className="lesson-hero auth-hero">
<div>
<p className="kicker">MODULE 02 / WEB & API</p>
<h1>Auth:把信任<br /><em>接到正確的位置</em></h1>
<p>Entra ID 是服務,OIDC 是協定,SSO 是結果。</p>
</div>
<div className="auth-hero-actions">
<div className={`module-status ${completed ? "done" : ""}`}><span>{completed ? "✓" : "02"}</span><div><small>MODULE STATUS</small><b>{completed ? "已完成" : "學習中"}</b></div></div>
<div className={`module-status ${completed ? "done" : ""}`}><span>{completed ? "✓" : "09"}</span><div><b>{completed ? "已完成" : "學習中"}</b></div></div>
<a className="button auth-deck-link" href={`${import.meta.env.BASE_URL}entra-oidc-sso-deck.html`}>開啟簡報 <span>→</span></a>
</div>
</header>

<TopicOrientation orientation={orientation} />
<TopicOrientation orientation={orientation} showMeta={false} />

<section className="auth-model" aria-label="OIDC 設定關係">
<div className="auth-node"><small>DIRECTORY</small><strong>Tenant ID</strong><code>{demoConfig.tenantId}</code></div>
Expand All @@ -39,7 +38,7 @@ export function AuthLesson({ completed, onOpenLab, orientation }: { completed: b
</section>

<section className="lab-cta">
<div><p className="kicker">CONFIGURATION LAB</p><h2>把七個值放回登入流程。</h2></div>
<div><h2>把七個值放回登入流程。</h2></div>
<button className="button light" onClick={onOpenLab}>進入 Auth Lab <span>→</span></button>
</section>
</div>
Expand Down
20 changes: 9 additions & 11 deletions frontend/src/components/CliLab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ interface TerminalEntry {
exitCode?: number | null;
}

const initialHistory: readonly TerminalEntry[] = [
{ lines: ["CLI Workshop sandbox v1", "固定 project fixture 已準備好。輸入第一個指令開始。"] },
];
const initialHistory: readonly TerminalEntry[] = [];

function statusTone(state: CliLabState): TopicStatusTone {
if (state.phase === "completed") return "success";
Expand Down Expand Up @@ -71,24 +69,25 @@ export function CliLab({ onComplete }: { onComplete: () => void }) {
function reset() {
setCli(createCliLabState());
setCommand("");
setHistory([{ lines: ["Sandbox 已重設。從 pwd 重新開始。"] }]);
setHistory([{ lines: ["已重設。從 pwd 重新開始。"] }]);
}

return (
<TopicLabShell
eyebrow="INTERACTIVE LAB / CLI"
title={<>在固定工作目錄中<br /><em>讀懂命令列</em></>}
className="foundation-lab-shell"
showMeta={false}
title={<>在工作目錄中<br /><em>讀懂命令列</em></>}
progressLabel={cliLabIsComplete(cli) ? "完成" : `${completedCount} / ${cliLessonSteps.length}`}
progress={progress}
onReset={reset}
>
<TopicStatusFeedback tone={statusTone(cli)} message={statusMessage(cli)} />

<div className="lab-layout cli-lab-layout">
<section className="terminal-panel" aria-label="CLI 命令列模擬器">
<section className="terminal-panel" aria-label="CLI 命令列">
<div className="terminal-top">
<span className="terminal-dots" aria-hidden="true"><i /><i /><i /></span>
<b>cli-workshop — {cli.cwd}</b>
<b>{cli.cwd}</b>
<span aria-label={`目前 exit code ${cli.exitCode ?? "尚未執行"}`}>exit {cli.exitCode ?? "—"}</span>
</div>
<div className="terminal-output" role="log" aria-live="polite" aria-label="命令列輸出">
Expand Down Expand Up @@ -117,15 +116,14 @@ export function CliLab({ onComplete }: { onComplete: () => void }) {
/>
<button type="submit" disabled={!command.trim() || cliLabIsComplete(cli)}>執行</button>
</form>
<p id="cli-command-help" className="sr-only">輸入固定 fixture 支援的命令,按 Enter 或執行送出。</p>
<p id="cli-command-help" className="sr-only">輸入教材中的命令,按 Enter 或執行送出。</p>
</section>

<aside className="mission-panel">
<p className="kicker">CURRENT MISSION</p>
{cliLabIsComplete(cli) ? (
<TopicCompletionCard
title="命令列流程完成"
description="你已能確認 cwd、讀取 fixture、解讀輸出,並用固定檢查完成一次可重複流程。"
description="你已能確認 cwd、讀取檔案、解讀輸出,並完成一次可重複流程。"
onReset={reset}
/>
) : (
Expand Down
14 changes: 6 additions & 8 deletions frontend/src/components/GitLab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ interface TerminalEntry {
accepted?: boolean;
}

const initialHistory: readonly TerminalEntry[] = [
{ lines: ["Git cowork sandbox v2", "固定 hosted repository 與 pipeline fixture 已準備好。"] },
];
const initialHistory: readonly TerminalEntry[] = [];

function statusTone(state: GitReleaseState, accepted: boolean): TopicStatusTone {
if (!accepted) return "error";
Expand Down Expand Up @@ -74,7 +72,8 @@ export function GitLab({ onComplete }: { onComplete: () => void }) {

return (
<TopicLabShell
eyebrow="INTERACTIVE LAB / GIT COWORK"
eyebrow=""
className="git-lab-shell"
title={<>把一個功能<br /><em>送進 pipeline</em></>}
progressLabel={completed ? "完成" : `${state.completedStepIds.length} / ${GIT_RELEASE_STEPS.length} STEPS`}
progress={gitReleaseProgress(state)}
Expand All @@ -90,7 +89,7 @@ export function GitLab({ onComplete }: { onComplete: () => void }) {
<>
<div className="lab-layout git-release-layout">
<section className="terminal-panel" aria-label="Git 指令終端機">
<div className="terminal-top"><span className="terminal-dots"><i /><i /><i /></span><b>git-cowork — {state.localBranch}</b><button type="button" onClick={reset}>重設</button></div>
<div className="terminal-top"><span className="terminal-dots"><i /><i /><i /></span><button type="button" onClick={reset}>重設</button></div>
<div className="terminal-output" aria-live="polite">
{history.map((entry, index) => (
<div className={`terminal-entry ${entry.accepted === false ? "error" : ""}`} key={`${index}-${entry.command ?? "system"}`}>
Expand All @@ -107,7 +106,6 @@ export function GitLab({ onComplete }: { onComplete: () => void }) {
</section>

<aside className="mission-panel" aria-label="Git cowork 狀態">
<p className="kicker">CURRENT MISSION</p>
<label className="git-provider-picker" htmlFor="git-provider">練習平台
<select
id="git-provider"
Expand Down Expand Up @@ -140,7 +138,7 @@ export function GitLab({ onComplete }: { onComplete: () => void }) {
</div>

<div className="git-release-actions remote-action-panel">
<div className="remote-panel-heading"><div><p className="kicker">WORKFLOW ACTIONS</p><h2>選擇一個操作,理解它改變哪一層</h2></div><span className="remote-lab-meta">{state.provider === "github" ? "PR" : "MERGE REQUEST"} · {state.targetBranch}</span></div>
<div className="remote-panel-heading"><div><h2>選擇一個操作,理解它改變哪一層</h2></div><span className="remote-lab-meta">{state.provider === "github" ? "PR" : "MERGE REQUEST"} · {state.targetBranch}</span></div>
<div className="remote-action-list">
{GIT_RELEASE_STEPS.map((step, index) => {
const done = state.completedStepIds.includes(step.id);
Expand All @@ -156,7 +154,7 @@ export function GitLab({ onComplete }: { onComplete: () => void }) {
</div>

<div className="git-release-pipeline remote-context-panel">
<div className="section-heading"><div><p className="kicker">PIPELINE JOBS</p><h2>push 之後 runner 執行什麼?</h2></div><p>每個 job 都是可讀的 fixture 結果,不連線真實 CI provider。</p></div>
<div className="section-heading"><div><h2>push 之後 runner 執行什麼?</h2></div></div>
<div className="git-job-grid">
{GIT_RELEASE_PIPELINE_JOBS.map((job) => <div key={job} className={state.pipelineJobs.includes(job) ? "passed" : "pending"}><span>{state.pipelineJobs.includes(job) ? "✓" : "○"}</span><b>{job}</b></div>)}
</div>
Expand Down
23 changes: 11 additions & 12 deletions frontend/src/components/GitLesson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ import { gitCommandGuide, gitLessons, gitPipeline, gitWorkflow } from "../conten

export function GitLesson({ completed, onOpenLab, orientation }: { completed: boolean; onOpenLab: () => void; orientation: LessonOrientation }) {
return (
<div className="page lesson-page">
<div className="page lesson-page git-lesson-page">
<header className="lesson-hero">
<div>
<p className="kicker">MODULE 01 / FOUNDATIONS</p>
<h1>Git:讓改變<br /><em>有跡可循</em></h1>
<p>先建立本地歷史與遠端協作的心智模型,再到 Lab 親手走過 clone → commit → push → pipeline → merge。</p>
</div>
<div className={`module-status ${completed ? "done" : ""}`}>
<span>{completed ? "✓" : "01"}</span>
<div><small>MODULE STATUS</small><b>{completed ? "已完成" : "學習中"}</b></div>
<div><b>{completed ? "已完成" : "學習中"}</b></div>
</div>
</header>

<TopicOrientation orientation={orientation} />
<TopicOrientation orientation={orientation} showMeta={false} />

<section className="mental-model">
<div className="section-heading">
<div><p className="kicker">MENTAL MODEL</p><h2>檔案如何變成歷史</h2></div>
<div><h2>檔案如何變成歷史</h2></div>
<p>每一步都在縮小「這次到底要記錄什麼」。</p>
</div>
<div className="workflow-row">
Expand All @@ -37,35 +36,35 @@ export function GitLesson({ completed, onOpenLab, orientation }: { completed: bo
<section className="lesson-list">
{gitLessons.map((lesson) => (
<article className="lesson-card" key={lesson.number}>
<div className="lesson-index"><span>{lesson.number}</span><small>{lesson.eyebrow}</small></div>
<div className="lesson-index"><span>{lesson.number}</span></div>
<div className="lesson-copy">
<h2>{lesson.title}</h2>
<p>{lesson.body}</p>
<code><span>$</span> {lesson.command}</code>
</div>
<aside><small>記住這句</small><p>{lesson.takeaway}</p></aside>
<aside><p>{lesson.takeaway}</p></aside>
</article>
))}
</section>

<section className="lesson-list git-command-guide" aria-label="Git 指令與使用時機">
<div className="section-heading"><div><p className="kicker">COMMAND DECISION GUIDE</p><h2>知道指令,更要知道什麼時候用</h2></div><p>每個操作都要能說明它改變了哪一層狀態。</p></div>
<div className="section-heading"><div><h2>知道指令,更要知道什麼時候用</h2></div></div>
{gitCommandGuide.map((guide, index) => (
<article className="lesson-card" key={guide.id}>
<div className="lesson-index"><span>{String(index + 1).padStart(2, "0")}</span><small>{guide.category}</small></div>
<div className="lesson-index"><span>{String(index + 1).padStart(2, "0")}</span></div>
<div className="lesson-copy">
<h2>{guide.title}</h2>
<p>{guide.body}</p>
<code><span>$</span> {guide.command}</code>
</div>
<aside><small>什麼時候用</small><p>{guide.when}</p><small>記住這句</small><p>{guide.takeaway}</p></aside>
<aside><p>{guide.when}</p><p>{guide.takeaway}</p></aside>
</article>
))}
</section>

<section className="mental-model git-pipeline-model" aria-label="Commit 到部署的 pipeline">
<div className="section-heading">
<div><p className="kicker">FROM COMMIT TO DEPLOY</p><h2>push 之後,平台做了什麼?</h2></div>
<div><h2>push 之後,平台做了什麼?</h2></div>
<p>Git command 改變 repository;hosted platform 再依規則啟動 review、pipeline 與部署。</p>
</div>
<div className="workflow-row">
Expand All @@ -79,7 +78,7 @@ export function GitLesson({ completed, onOpenLab, orientation }: { completed: bo
</section>

<section className="lab-cta">
<div><p className="kicker">READY TO PRACTICE?</p><h2>看懂了,現在親手做一次。</h2><p>一條 cowork workflow,約 12 分鐘。輸錯不會破壞任何真實 repository。</p></div>
<div><h2>看懂了,現在親手做一次。</h2></div>
<button className="button light" onClick={onOpenLab}>進入 Git Lab <span>→</span></button>
</section>
</div>
Expand Down
Loading
Loading