Skip to content

Commit c976cb3

Browse files
committed
add tutorial progress tracker and quiz
1 parent 4ec1e6f commit c976cb3

15 files changed

Lines changed: 1092 additions & 17 deletions

File tree

astro.config.mjs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export default defineConfig({
2727
github: 'https://github.com/sannybuilder/tutorial',
2828
discord: 'https://sannybuilder.com/discord',
2929
},
30+
components: {
31+
PageTitle: './src/components/PageTitle.astro',
32+
Footer: './src/components/Footer.astro',
33+
},
3034
tableOfContents: false,
3135
expressiveCode: {
3236
themes: ['dracula', 'solarized-light'],
@@ -60,8 +64,9 @@ export default defineConfig({
6064
{ label: 'Setting up the environment', slug: 'setup' },
6165
{ label: 'CLEO Library', slug: 'cleo-library' },
6266
{ label: 'Compiling scripts with Sanny Builder', slug: 'sanny-builder' },
63-
{ label: 'Stripped main.scm', slug: 'stripped-scm' },
64-
],
67+
{ label: 'Stripped main.scm', slug: 'stripped-scm' },
68+
{ label: 'Quiz', slug: 'quiz-ch00' },
69+
],
6570
},
6671
{
6772
label: 'Chapter I: Hello, World!',
@@ -73,8 +78,9 @@ export default defineConfig({
7378
{ label: 'Strings', slug: 'strings' },
7479
{ label: 'Comments', slug: 'comments' },
7580
{ label: '$CLEO Directive', slug: 'cleo-directive' },
76-
{ label: 'Hands-on: Showing a Message', slug: 'script-show-message' },
77-
],
81+
{ label: 'Hands-on: Showing a Message', slug: 'script-show-message' },
82+
{ label: 'Quiz', slug: 'quiz-ch01' },
83+
],
7884
},
7985
{
8086
label: 'Chapter II: Introducing Loops',
@@ -84,8 +90,9 @@ export default defineConfig({
8490
{ label: 'WAIT command', slug: 'wait' },
8591
{ label: 'WHILE Loop', slug: 'while' },
8692
{ label: 'Infinite Loop with WHILE TRUE', slug: 'while-true' },
87-
{ label: 'Hands-on: Spawning a Vehicle', slug: 'script-spawn-vehicle' },
88-
],
93+
{ label: 'Hands-on: Spawning a Vehicle', slug: 'script-spawn-vehicle' },
94+
{ label: 'Quiz', slug: 'quiz-ch02' },
95+
],
8996
},
9097
{
9198
label: 'Chapter III: To Be Or Not To Be',
@@ -94,8 +101,9 @@ export default defineConfig({
94101
{ label: 'Conditions', slug: 'conditions' },
95102
{ label: 'IF..ELSE', slug: 'else' },
96103
{ label: 'Negating Conditions', slug: 'negating-conditions' },
97-
{ label: 'Multiple Conditions', slug: 'multiple-conditions' },
98-
],
104+
{ label: 'Multiple Conditions', slug: 'multiple-conditions' },
105+
{ label: 'Quiz', slug: 'quiz-ch03' },
106+
],
99107
},
100108
{
101109
label: "Chapter IV: Working with Text",
@@ -106,9 +114,9 @@ export default defineConfig({
106114
{label: 'Formatted Messages', slug: 'formatted-messages'},
107115
{label: 'Text Draws', slug: 'text-draws'},
108116
{label: 'Debug Messages', slug: 'debug-messages'},
109-
{label: 'String Variables', slug: 'string-variables'},
110-
111-
]
117+
{label: 'String Variables', slug: 'string-variables'},
118+
{ label: 'Quiz', slug: 'quiz-ch04' },
119+
]
112120
},
113121
{
114122
label: "Chapter V: Advanced Loops",
@@ -118,8 +126,9 @@ export default defineConfig({
118126
{ label: 'FOR Loop', slug: 'for-loop' },
119127
{ label: 'REPEAT..UNTIL Loop', slug: 'repeat-until' },
120128
{ label: 'Continue and Break', slug: 'continue-break' },
121-
{ label: 'Hands-on: Bonus Counter', slug: 'script-bonus-counter' },
122-
]
129+
{ label: 'Hands-on: Bonus Counter', slug: 'script-bonus-counter' },
130+
{ label: 'Quiz', slug: 'quiz-ch05' },
131+
]
123132
},
124133
{
125134
label: "Chapter VI: One Name, Many Values",
@@ -128,8 +137,9 @@ export default defineConfig({
128137
{ label: 'Arrays', slug: 'arrays' },
129138
{ label: 'Arrays and Loops', slug: 'arrays-and-loops' },
130139
{ label: 'Spread Operator', slug: 'spread' },
131-
{ label: 'Hands-on: Checkpoint Hunt', slug: 'script-checkpoint-hunt' },
132-
]
140+
{ label: 'Hands-on: Checkpoint Hunt', slug: 'script-checkpoint-hunt' },
141+
{ label: 'Quiz', slug: 'quiz-ch06' },
142+
]
133143
},
134144
{
135145
label: "Chapter VII: Code Reuse with Functions",
@@ -140,8 +150,9 @@ export default defineConfig({
140150
{ label: 'Returning Values', slug: 'return-values' },
141151
{ label: 'Logical Functions', slug: 'logical-functions' },
142152
{ label: 'Optional Return', slug: 'optional-return' },
143-
{ label: 'Hands-on: Vehicle Roulette', slug: 'script-vehicle-roulette' },
144-
]
153+
{ label: 'Hands-on: Vehicle Roulette', slug: 'script-vehicle-roulette' },
154+
{ label: 'Quiz', slug: 'quiz-ch07' },
155+
]
145156
}
146157
],
147158
}),
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
---
2+
// This component is entirely client-side driven.
3+
// It reads progress from localStorage and links to the first incomplete lesson.
4+
---
5+
6+
<div class="tutorial-status" id="tutorial-status" style="display: none;">
7+
<div class="progress-section">
8+
<div class="stat-row">
9+
<span class="stat-label">Lessons Completed</span>
10+
<span class="stat-value" id="lesson-stat"></span>
11+
</div>
12+
<div class="stat-row">
13+
<span class="stat-label">Quizzes Passed</span>
14+
<span class="stat-value" id="quiz-stat"></span>
15+
</div>
16+
<div class="stat-row stat-row-total">
17+
<span class="stat-label">Progress Total</span>
18+
<span class="stat-value" id="total-stat"></span>
19+
</div>
20+
</div>
21+
22+
<div class="continue-section" id="continue-section" style="display: none;">
23+
<a class="continue-link" id="continue-link" href="/">
24+
<span>Continue with </span>
25+
<span class="continue-title" id="continue-title"></span>
26+
<span class="continue-arrow" aria-hidden="true">&rarr;</span>
27+
</a>
28+
</div>
29+
</div>
30+
31+
<script>
32+
function initTutorialStatus() {
33+
const PROGRESS_KEY = 'tutorial-progress';
34+
35+
const container = document.getElementById('tutorial-status');
36+
if (!container) return;
37+
38+
// Collect all sidebar links, separating lessons from quizzes
39+
const allSidebarLinks = document.querySelectorAll<HTMLAnchorElement>(
40+
'.sidebar-content a[href]'
41+
);
42+
const lessons: { slug: string; link: HTMLAnchorElement }[] = [];
43+
const quizzes: { slug: string; link: HTMLAnchorElement }[] = [];
44+
allSidebarLinks.forEach((link) => {
45+
const href = link.getAttribute('href') || '';
46+
const slug = href.replace(/^\/|\/$/g, '');
47+
if (!slug) return;
48+
if (slug.startsWith('quiz-')) {
49+
quizzes.push({ slug, link });
50+
} else {
51+
lessons.push({ slug, link });
52+
}
53+
});
54+
55+
const totalLessons = lessons.length;
56+
if (totalLessons === 0) return;
57+
58+
// Read progress from localStorage
59+
let progress: Record<string, boolean> = {};
60+
try {
61+
progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}');
62+
} catch {}
63+
64+
// Count completed
65+
const completedLessons = lessons.filter(({ slug }) => progress[slug]).length;
66+
const totalQuizzes = quizzes.length;
67+
const completedQuizzes = quizzes.filter(({ slug }) => progress[slug]).length;
68+
69+
// Update lesson stat
70+
const lessonStat = document.getElementById('lesson-stat');
71+
if (lessonStat) {
72+
lessonStat.textContent = `${completedLessons} of ${totalLessons}`;
73+
}
74+
75+
// Update quiz stat
76+
const quizStat = document.getElementById('quiz-stat');
77+
if (quizStat && totalQuizzes > 0) {
78+
quizStat.textContent = `${completedQuizzes} of ${totalQuizzes}`;
79+
}
80+
81+
// Update total progress percentage
82+
const totalStat = document.getElementById('total-stat');
83+
if (totalStat) {
84+
const totalItems = totalLessons + totalQuizzes;
85+
const totalCompleted = completedLessons + completedQuizzes;
86+
const pct = Math.round((totalCompleted / totalItems) * 100);
87+
totalStat.textContent = `${pct} of 100%`;
88+
}
89+
90+
container.style.display = '';
91+
92+
// Find the first incomplete lesson (not quiz) and link to it
93+
if (completedLessons < totalLessons) {
94+
const firstIncomplete = lessons.find(({ slug }) => !progress[slug]);
95+
if (firstIncomplete) {
96+
const href = firstIncomplete.link.getAttribute('href') || '';
97+
const label = firstIncomplete.link.textContent?.trim() || '';
98+
99+
const continueSection = document.getElementById('continue-section');
100+
const continueLink = document.getElementById('continue-link') as HTMLAnchorElement | null;
101+
const continueTitle = document.getElementById('continue-title');
102+
103+
if (continueSection && continueLink && continueTitle && label) {
104+
continueTitle.textContent = label;
105+
continueLink.href = href;
106+
continueSection.style.display = '';
107+
}
108+
}
109+
}
110+
}
111+
112+
initTutorialStatus();
113+
document.addEventListener('astro:after-swap', initTutorialStatus);
114+
</script>
115+
116+
<style>
117+
.tutorial-status {
118+
margin-top: 1.5rem;
119+
padding: 1rem 1.25rem;
120+
border: 1px solid var(--sl-color-hairline);
121+
border-radius: 0.5rem;
122+
background: var(--sl-color-bg-nav);
123+
}
124+
125+
.stat-row {
126+
display: flex;
127+
justify-content: space-between;
128+
align-items: baseline;
129+
font-size: var(--sl-text-sm);
130+
}
131+
132+
.stat-label {
133+
color: var(--sl-color-gray-2);
134+
}
135+
136+
.stat-value {
137+
color: var(--sl-color-gray-3);
138+
font-variant-numeric: tabular-nums;
139+
}
140+
141+
.stat-value:empty::after {
142+
content: '\2014';
143+
color: var(--sl-color-gray-4);
144+
}
145+
146+
.stat-row-total {
147+
margin-top: 0.25rem;
148+
padding-top: 0.5rem;
149+
border-top: 1px solid var(--sl-color-hairline);
150+
font-weight: 600;
151+
color: var(--sl-color-white);
152+
}
153+
154+
.stat-row-total .stat-label,
155+
.stat-row-total .stat-value {
156+
color: var(--sl-color-white);
157+
}
158+
159+
.continue-section {
160+
margin-top: 0.75rem;
161+
padding-top: 0.75rem;
162+
border-top: 1px solid var(--sl-color-hairline);
163+
}
164+
165+
.continue-link {
166+
display: inline-flex;
167+
align-items: center;
168+
gap: 0.25rem;
169+
font-size: var(--sl-text-sm);
170+
color: var(--sl-color-text-accent);
171+
text-decoration: none;
172+
}
173+
174+
.continue-link:hover {
175+
text-decoration: underline;
176+
}
177+
178+
.continue-title {
179+
font-weight: 600;
180+
}
181+
182+
.continue-arrow {
183+
font-size: 1rem;
184+
}
185+
</style>

0 commit comments

Comments
 (0)