@curio-sd/e-module-builder 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -15
- package/bin/cli.js +2 -0
- package/build-pdf.mjs +452 -0
- package/build.mjs +168 -75
- package/package.json +3 -1
- package/src/css/main.css +124 -50
- package/src/js/checklist.js +1 -1
- package/src/js/exercises/exercise-runner.js +4 -1
- package/src/js/exercises/exercise-shared.js +1 -1
- package/src/js/exercises/external-exercise.js +3 -4
- package/src/js/exercises/hub.js +5 -1
- package/src/js/exercises/load-exercise.js +5 -142
- package/src/js/exercises/theory-panel.js +95 -0
- package/src/js/home.js +1 -1
- package/src/js/nav.js +63 -8
- package/src/js/theory.js +0 -2
- package/templates/index.html +17 -9
- package/templates/pages/inleveropdracht.html +32 -0
- package/templates/pages/meetmoment-praktijk.html +35 -0
- package/templates/pages/meetmoment-theorie.html +29 -0
- package/templates/pages/{toets.html → meetmoment.html} +11 -4
- package/templates/pages/oefening.html +29 -0
- package/templates/pages/oefeningen.html +46 -38
- package/templates/pages/theorie.html +6 -1
- package/vite.config.js +3 -1
- package/templates/pages/toets-praktijk.html +0 -25
- package/templates/pages/toets-theorie.html +0 -25
|
@@ -66,6 +66,9 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
|
|
|
66
66
|
|
|
67
67
|
if (!containerInput || !preview) return
|
|
68
68
|
|
|
69
|
+
const outerClassMatch = exercise.previewHtml.match(/<\w+[^>]*\bclass="([^"]*)"/)
|
|
70
|
+
const gridClass = (outerClassMatch?.[1] ?? '').split(/\s+/)[0] || 'grid-container'
|
|
71
|
+
|
|
69
72
|
const updatePreview = () => {
|
|
70
73
|
const areas = containerInput.value.trim()
|
|
71
74
|
const itemStyles = Array.from(selects)
|
|
@@ -74,7 +77,7 @@ export function initAreasExercise(exercise, { onSolved } = {}) {
|
|
|
74
77
|
|
|
75
78
|
preview.innerHTML = `
|
|
76
79
|
<style>
|
|
77
|
-
|
|
80
|
+
.${gridClass} {
|
|
78
81
|
display: grid;
|
|
79
82
|
grid-template-columns: ${exercise.gridColumns || '1fr 1fr 1fr'};
|
|
80
83
|
grid-template-areas: ${areas || '"a b c"'};
|
|
@@ -12,7 +12,7 @@ export function renderExerciseMeta(exercise) {
|
|
|
12
12
|
diffEl.textContent = `Oefening ${String(exercise.id).padStart(2, '0')} · ${label}`
|
|
13
13
|
}
|
|
14
14
|
if (titleEl) titleEl.textContent = exercise.title
|
|
15
|
-
if (descEl) descEl.
|
|
15
|
+
if (descEl) descEl.innerHTML = exercise.descriptionInlineHtml ?? ''
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function markExerciseSolved(week, id) {
|
|
@@ -38,7 +38,7 @@ export function initExternalExercise(exercise, weekNum, { onSolved } = {}) {
|
|
|
38
38
|
const deliverablesEl = document.querySelector('[data-external-deliverables]')
|
|
39
39
|
const feedback = document.querySelector('[data-feedback]')
|
|
40
40
|
|
|
41
|
-
if (taskEl) taskEl.innerHTML = exercise.
|
|
41
|
+
if (taskEl) taskEl.innerHTML = exercise.taskHtml || exercise.descriptionInlineHtml || ''
|
|
42
42
|
if (stepsEl) stepsEl.innerHTML = renderSteps(exercise.steps)
|
|
43
43
|
if (envEl) envEl.textContent = exercise.environment || 'Je eigen omgeving (editor, server of tool)'
|
|
44
44
|
if (deliverablesEl) {
|
|
@@ -48,15 +48,14 @@ export function initExternalExercise(exercise, weekNum, { onSolved } = {}) {
|
|
|
48
48
|
document.querySelector('[data-hint]')?.addEventListener('click', () => {
|
|
49
49
|
showFeedback(
|
|
50
50
|
feedback,
|
|
51
|
-
|
|
51
|
+
exercise.hintHtml || '<p class="text-zinc-600">Werk de stappen rustig door in je eigen omgeving.</p>'
|
|
52
52
|
)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
document.querySelector('[data-solution]')?.addEventListener('click', () => {
|
|
56
|
-
const solution = exercise.solution || 'Vergelijk je werk met de theorie en vraag feedback aan je docent.'
|
|
57
56
|
showFeedback(
|
|
58
57
|
feedback,
|
|
59
|
-
`<p class="font-medium text-zinc-900">Voorbeelduitwerking</p
|
|
58
|
+
`<p class="font-medium text-zinc-900">Voorbeelduitwerking</p>${exercise.solutionHtml || '<p class="mt-2 text-sm text-zinc-600">Vergelijk je werk met de theorie en vraag feedback aan je docent.</p>'}`
|
|
60
59
|
)
|
|
61
60
|
})
|
|
62
61
|
|
package/src/js/exercises/hub.js
CHANGED
|
@@ -3,6 +3,10 @@ import { sitePath } from '../site-path.js'
|
|
|
3
3
|
|
|
4
4
|
const DIFFICULTY = ['', 'Beginner', 'Beginner', 'Gemiddeld', 'Gemiddeld', 'Gevorderd', 'Gevorderd', 'Expert', 'Expert']
|
|
5
5
|
|
|
6
|
+
function stripHtml(html) {
|
|
7
|
+
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export function initExerciseHub(weekData, weekNum) {
|
|
7
11
|
const container = document.querySelector('[data-exercise-list]')
|
|
8
12
|
if (!container) return
|
|
@@ -36,7 +40,7 @@ export function initExerciseHub(weekData, weekNum) {
|
|
|
36
40
|
<h3 class="font-medium text-zinc-900">${ex.title}</h3>
|
|
37
41
|
${done ? '<span class="badge-done">Voltooid</span>' : ''}
|
|
38
42
|
</div>
|
|
39
|
-
<p class="mt-1.5 text-sm leading-relaxed text-zinc-500">${ex.description}</p>
|
|
43
|
+
<p class="mt-1.5 text-sm leading-relaxed text-zinc-500">${ex.descriptionInlineHtml ?? ex.description ?? ''}</p>
|
|
40
44
|
${modeBadge}
|
|
41
45
|
</div>
|
|
42
46
|
</a>
|
|
@@ -1,107 +1,59 @@
|
|
|
1
1
|
import { renderExerciseMeta, markExerciseSolved } from './exercise-shared.js'
|
|
2
2
|
import { sitePath } from '../site-path.js'
|
|
3
|
-
|
|
4
3
|
import { initExternalExercise } from './external-exercise.js'
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { initTheoryPanel } from './theory-panel.js'
|
|
7
5
|
|
|
8
6
|
async function loadWeekData(weekNum) {
|
|
9
|
-
|
|
10
7
|
return import(`../../data/exercises/week${weekNum}.json`).then((m) => m.default)
|
|
11
|
-
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
10
|
function isExternalMode(weekData, exercise) {
|
|
17
|
-
|
|
18
11
|
return weekData?.mode === 'external' || exercise?.type === 'external'
|
|
19
|
-
|
|
20
12
|
}
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
24
14
|
function showMissingContent(weekNum) {
|
|
25
|
-
|
|
26
15
|
const panel = document.querySelector('[data-exercise-content]')
|
|
27
16
|
|
|
28
17
|
panel?.querySelector('[data-exercise-interactive]')?.classList.add('hidden')
|
|
29
|
-
|
|
30
18
|
panel?.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
31
|
-
|
|
32
19
|
panel?.querySelector('[data-hint]')?.closest('.mb-4')?.classList.add('hidden')
|
|
33
|
-
|
|
34
20
|
panel?.insertAdjacentHTML(
|
|
35
|
-
|
|
36
21
|
'beforeend',
|
|
37
|
-
|
|
38
22
|
`<div class="card border-l-4 border-l-amber-500">
|
|
39
|
-
|
|
40
23
|
<p class="font-medium text-zinc-900">Content ontbreekt</p>
|
|
41
|
-
|
|
42
|
-
<p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar is nog niet gegenereerd. Run in een terminal:</p>
|
|
43
|
-
|
|
44
|
-
<pre class="code-block mt-3 text-sm"><code>npm run generate-content</code></pre>
|
|
45
|
-
|
|
46
|
-
<p class="mt-2 text-sm text-zinc-500">Herlaad daarna deze pagina.</p>
|
|
47
|
-
|
|
24
|
+
<p class="mt-2 text-sm text-zinc-600">De oefening staat in de navigatie, maar de inhoud is niet beschikbaar.</p>
|
|
48
25
|
</div>`
|
|
49
|
-
|
|
50
26
|
)
|
|
51
|
-
|
|
52
27
|
}
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
56
29
|
export async function initExercisePage(weekNum) {
|
|
57
|
-
|
|
58
30
|
const params = new URLSearchParams(window.location.search)
|
|
59
|
-
|
|
60
31
|
const id = parseInt(params.get('id') || '1', 10)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
32
|
let weekData
|
|
65
33
|
|
|
66
34
|
try {
|
|
67
|
-
|
|
68
35
|
weekData = await loadWeekData(weekNum)
|
|
69
|
-
|
|
70
36
|
} catch {
|
|
71
|
-
|
|
72
37
|
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
73
|
-
|
|
74
38
|
'beforeend',
|
|
75
|
-
|
|
76
39
|
`<p class="text-red-600">Oefeningdata voor week ${weekNum} niet gevonden.</p>`
|
|
77
|
-
|
|
78
40
|
)
|
|
79
41
|
|
|
80
42
|
return
|
|
81
|
-
|
|
82
43
|
}
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
|
|
86
45
|
const exercise = weekData?.exercises?.find((e) => e.id === id)
|
|
87
46
|
|
|
88
|
-
|
|
89
|
-
|
|
90
47
|
if (!exercise) {
|
|
91
|
-
|
|
92
48
|
document.querySelector('[data-exercise-content]')?.insertAdjacentHTML(
|
|
93
|
-
|
|
94
49
|
'beforeend',
|
|
95
|
-
|
|
96
50
|
'<p class="text-red-600">Oefening niet gevonden.</p>'
|
|
97
|
-
|
|
98
51
|
)
|
|
99
52
|
|
|
100
53
|
return
|
|
101
|
-
|
|
102
54
|
}
|
|
103
55
|
|
|
104
|
-
|
|
56
|
+
initTheoryPanel(exercise.linked_theory)
|
|
105
57
|
|
|
106
58
|
if (!exercise.type || exercise.type === 'text') {
|
|
107
59
|
const panel = document.querySelector('[data-exercise-content]')
|
|
@@ -121,189 +73,100 @@ export async function initExercisePage(weekNum) {
|
|
|
121
73
|
}
|
|
122
74
|
|
|
123
75
|
if (isExternalMode(weekData, exercise)) {
|
|
124
|
-
|
|
125
76
|
const missing = !exercise.task?.trim() && !exercise.description?.trim()
|
|
126
77
|
|
|
127
78
|
if (missing) {
|
|
128
|
-
|
|
129
79
|
showMissingContent(weekNum)
|
|
130
|
-
|
|
131
80
|
return
|
|
132
|
-
|
|
133
81
|
}
|
|
134
82
|
|
|
135
83
|
initExternalExercise(exercise, weekNum, {
|
|
136
|
-
|
|
137
84
|
onSolved: () => markExerciseSolved(weekNum, exercise.id),
|
|
138
|
-
|
|
139
85
|
})
|
|
140
86
|
|
|
141
87
|
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
142
|
-
|
|
143
88
|
return
|
|
144
|
-
|
|
145
89
|
}
|
|
146
90
|
|
|
147
|
-
|
|
148
|
-
|
|
149
91
|
renderExerciseMeta(exercise)
|
|
150
92
|
|
|
151
|
-
|
|
152
|
-
|
|
153
93
|
const missingContent =
|
|
154
|
-
|
|
155
|
-
!exercise.starterCss?.trim() ||
|
|
156
|
-
|
|
94
|
+
(exercise.type === 'areas' ? !exercise.starterAreas?.trim() : !exercise.starterCss?.trim()) ||
|
|
157
95
|
!exercise.previewHtml?.trim() ||
|
|
158
|
-
|
|
159
96
|
(exercise.type !== 'areas' && !exercise.checks?.length)
|
|
160
97
|
|
|
161
|
-
|
|
162
|
-
|
|
163
98
|
if (missingContent) {
|
|
164
|
-
|
|
165
99
|
showMissingContent(weekNum)
|
|
166
|
-
|
|
167
100
|
return
|
|
168
|
-
|
|
169
101
|
}
|
|
170
102
|
|
|
171
|
-
|
|
172
|
-
|
|
173
103
|
const {
|
|
174
|
-
|
|
175
104
|
initCssPlayground,
|
|
176
|
-
|
|
177
105
|
initAreasExercise,
|
|
178
|
-
|
|
179
106
|
initResponsiveExercise,
|
|
180
|
-
|
|
107
|
+
renderAreaSelects,
|
|
181
108
|
} = await import('./exercise-runner.js')
|
|
182
109
|
|
|
183
|
-
|
|
184
|
-
|
|
185
110
|
document.querySelector('[data-exercise-external]')?.classList.add('hidden')
|
|
186
|
-
|
|
187
111
|
document.querySelector('[data-exercise-interactive]')?.classList.remove('hidden')
|
|
188
112
|
|
|
189
|
-
|
|
190
|
-
|
|
191
113
|
const cssPanel = document.querySelector('[data-exercise-css-panel]')
|
|
192
|
-
|
|
193
114
|
const areasPanel = document.querySelector('[data-exercise-areas-panel]')
|
|
194
|
-
|
|
195
115
|
const responsiveBar = document.querySelector('[data-responsive-bar]')
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
116
|
const onSolved = () => markExerciseSolved(weekNum, exercise.id)
|
|
200
117
|
|
|
201
|
-
|
|
202
|
-
|
|
203
118
|
if (exercise.type === 'areas') {
|
|
204
|
-
|
|
205
119
|
cssPanel?.classList.add('hidden')
|
|
206
|
-
|
|
207
120
|
areasPanel?.classList.remove('hidden')
|
|
208
|
-
|
|
209
121
|
responsiveBar?.classList.add('hidden')
|
|
210
|
-
|
|
211
122
|
document.querySelector('[data-solution]')?.classList.add('hidden')
|
|
212
123
|
|
|
213
|
-
|
|
214
|
-
|
|
215
124
|
const containerInput = document.querySelector('[data-areas-container]')
|
|
216
125
|
|
|
217
126
|
if (containerInput) containerInput.value = exercise.starterAreas || ''
|
|
218
127
|
|
|
219
|
-
|
|
220
|
-
|
|
221
128
|
const selectsEl = document.querySelector('[data-area-selects]')
|
|
222
129
|
|
|
223
130
|
if (selectsEl) {
|
|
224
|
-
|
|
225
131
|
selectsEl.innerHTML = renderAreaSelects(exercise.areaItems, exercise.areaOptions)
|
|
226
|
-
|
|
227
132
|
}
|
|
228
133
|
|
|
229
|
-
|
|
230
|
-
|
|
231
134
|
initAreasExercise(exercise, { onSolved })
|
|
232
|
-
|
|
233
135
|
} else {
|
|
234
|
-
|
|
235
136
|
areasPanel?.classList.add('hidden')
|
|
236
|
-
|
|
237
137
|
cssPanel?.classList.remove('hidden')
|
|
238
138
|
|
|
239
|
-
|
|
240
|
-
|
|
241
139
|
if (exercise.type === 'responsive') {
|
|
242
|
-
|
|
243
140
|
responsiveBar?.classList.remove('hidden')
|
|
244
|
-
|
|
245
141
|
initResponsiveExercise(exercise, { onSolved })
|
|
246
|
-
|
|
247
142
|
} else {
|
|
248
|
-
|
|
249
143
|
responsiveBar?.classList.add('hidden')
|
|
250
|
-
|
|
251
144
|
initCssPlayground(exercise, { onSolved })
|
|
252
|
-
|
|
253
145
|
}
|
|
254
|
-
|
|
255
146
|
}
|
|
256
147
|
|
|
257
|
-
|
|
258
|
-
|
|
259
148
|
renderNavButtons(weekNum, id, weekData.exercises.length)
|
|
260
|
-
|
|
261
149
|
}
|
|
262
150
|
|
|
263
|
-
|
|
264
|
-
|
|
265
151
|
function renderNavButtons(week, currentId, total) {
|
|
266
|
-
|
|
267
152
|
const prev = document.querySelector('[data-prev-exercise]')
|
|
268
|
-
|
|
269
153
|
const next = document.querySelector('[data-next-exercise]')
|
|
270
154
|
|
|
271
|
-
|
|
272
|
-
|
|
273
155
|
if (prev) {
|
|
274
|
-
|
|
275
156
|
if (currentId > 1) {
|
|
276
|
-
|
|
277
157
|
prev.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId - 1}`
|
|
278
|
-
|
|
279
158
|
prev.classList.remove('hidden')
|
|
280
|
-
|
|
281
159
|
} else {
|
|
282
|
-
|
|
283
160
|
prev.classList.add('hidden')
|
|
284
|
-
|
|
285
161
|
}
|
|
286
|
-
|
|
287
162
|
}
|
|
288
163
|
|
|
289
|
-
|
|
290
|
-
|
|
291
164
|
if (next) {
|
|
292
|
-
|
|
293
165
|
if (currentId < total) {
|
|
294
|
-
|
|
295
166
|
next.href = `${sitePath(`/pages/week${week}-oefening.html`)}?id=${currentId + 1}`
|
|
296
|
-
|
|
297
167
|
next.classList.remove('hidden')
|
|
298
|
-
|
|
299
168
|
} else {
|
|
300
|
-
|
|
301
169
|
next.classList.add('hidden')
|
|
302
|
-
|
|
303
170
|
}
|
|
304
|
-
|
|
305
171
|
}
|
|
306
|
-
|
|
307
172
|
}
|
|
308
|
-
|
|
309
|
-
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { sitePath } from '../site-path.js'
|
|
2
|
+
|
|
3
|
+
const WEEK_RE = /^([a-zA-Z]+)(\d+)$/
|
|
4
|
+
|
|
5
|
+
const BOOK_SVG = `<svg class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.966 8.966 0 0 0-6 2.292m0-14.25v14.25" /></svg>`
|
|
6
|
+
|
|
7
|
+
export function parseLinkedTheory(linkedTheory) {
|
|
8
|
+
if (!Array.isArray(linkedTheory) || linkedTheory.length === 0) return []
|
|
9
|
+
|
|
10
|
+
return linkedTheory
|
|
11
|
+
.map((entry) => {
|
|
12
|
+
if (typeof entry !== 'string') return null
|
|
13
|
+
const m = entry.trim().match(WEEK_RE)
|
|
14
|
+
if (!m) return null
|
|
15
|
+
const prefix = m[1]
|
|
16
|
+
const num = parseInt(m[2], 10)
|
|
17
|
+
const label = `${prefix.charAt(0).toUpperCase()}${prefix.slice(1)} ${num}`
|
|
18
|
+
return { key: entry.trim(), label, num, dirName: entry.trim() }
|
|
19
|
+
})
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function theoryIframeSrc(dirName) {
|
|
24
|
+
return sitePath(`/pages/${dirName}-theorie.html`) + '?embedded=1'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function initTheoryPanel(linkedTheory) {
|
|
28
|
+
const toggle = document.querySelector('[data-theory-toggle]')
|
|
29
|
+
const panel = document.querySelector('[data-theory-panel]')
|
|
30
|
+
|
|
31
|
+
const tabs = parseLinkedTheory(linkedTheory)
|
|
32
|
+
|
|
33
|
+
if (!tabs.length) {
|
|
34
|
+
toggle?.classList.add('hidden')
|
|
35
|
+
panel?.classList.add('panel-hidden')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toggle?.classList.remove('hidden')
|
|
40
|
+
|
|
41
|
+
const tabsEl = panel?.querySelector('[data-theory-tabs]')
|
|
42
|
+
const iframe = panel?.querySelector('[data-theory-iframe]')
|
|
43
|
+
const loader = panel?.querySelector('[data-theory-loader]')
|
|
44
|
+
|
|
45
|
+
if (!tabsEl || !iframe || !panel) return
|
|
46
|
+
|
|
47
|
+
function showLoader() { loader?.classList.remove('theory-panel-loader-hidden') }
|
|
48
|
+
function hideLoader() { loader?.classList.add('theory-panel-loader-hidden') }
|
|
49
|
+
|
|
50
|
+
iframe.addEventListener('load', hideLoader)
|
|
51
|
+
|
|
52
|
+
tabsEl.innerHTML = tabs
|
|
53
|
+
.map(
|
|
54
|
+
(t, i) =>
|
|
55
|
+
`<button class="theory-tab${i === 0 ? ' theory-tab-active' : ''}" data-tab="${t.key}" type="button">${t.label}</button>`
|
|
56
|
+
)
|
|
57
|
+
.join('')
|
|
58
|
+
|
|
59
|
+
function activateTab(key) {
|
|
60
|
+
tabsEl.querySelectorAll('[data-tab]').forEach((btn) => {
|
|
61
|
+
btn.classList.toggle('theory-tab-active', btn.dataset.tab === key)
|
|
62
|
+
})
|
|
63
|
+
const tab = tabs.find((t) => t.key === key)
|
|
64
|
+
if (tab) {
|
|
65
|
+
showLoader()
|
|
66
|
+
iframe.src = theoryIframeSrc(tab.dirName)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
tabsEl.addEventListener('click', (e) => {
|
|
71
|
+
const btn = e.target.closest('[data-tab]')
|
|
72
|
+
if (btn) activateTab(btn.dataset.tab)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
activateTab(tabs[0].key)
|
|
76
|
+
|
|
77
|
+
const closeBtn = panel.querySelector('[data-theory-panel-close]')
|
|
78
|
+
closeBtn?.addEventListener('click', () => {
|
|
79
|
+
panel.classList.add('panel-hidden')
|
|
80
|
+
toggle?.setAttribute('aria-expanded', 'false')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
toggle?.addEventListener('click', () => {
|
|
84
|
+
const isHidden = panel.classList.contains('panel-hidden')
|
|
85
|
+
panel.classList.toggle('panel-hidden', !isHidden)
|
|
86
|
+
toggle.setAttribute('aria-expanded', String(isHidden))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
toggle?.setAttribute('aria-expanded', 'false')
|
|
90
|
+
|
|
91
|
+
// Enable the slide transition after the first frame has been committed so
|
|
92
|
+
// the panel doesn't animate from its off-screen position on page load.
|
|
93
|
+
// Double-rAF ensures this works even when initTheoryPanel is called synchronously.
|
|
94
|
+
requestAnimationFrame(() => requestAnimationFrame(() => panel.classList.add('panel-ready')))
|
|
95
|
+
}
|
package/src/js/home.js
CHANGED
package/src/js/nav.js
CHANGED
|
@@ -13,9 +13,7 @@ function buildNavItems() {
|
|
|
13
13
|
})
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
items.push(page)
|
|
18
|
-
}
|
|
16
|
+
items.push(manifest.nav.assessmentSection)
|
|
19
17
|
|
|
20
18
|
return items
|
|
21
19
|
}
|
|
@@ -69,16 +67,68 @@ function renderNavGroup(group) {
|
|
|
69
67
|
? `<p class="mt-0.5 text-[10px] font-normal leading-snug text-zinc-500">${group.title}</p>`
|
|
70
68
|
: ''
|
|
71
69
|
return `
|
|
72
|
-
<div class="space-y-0.5">
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
|
|
70
|
+
<div class="space-y-0.5" data-nav-group="${group.label}">
|
|
71
|
+
<button class="nav-group-toggle" data-group-toggle="${group.label}">
|
|
72
|
+
<div>
|
|
73
|
+
<p class="text-[10px] font-semibold uppercase tracking-[0.14em] text-zinc-400">${group.label}</p>
|
|
74
|
+
${title}
|
|
75
|
+
</div>
|
|
76
|
+
<svg class="nav-group-chevron" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
77
|
+
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
|
78
|
+
</svg>
|
|
79
|
+
</button>
|
|
80
|
+
<div class="nav-group-children" data-group-children="${group.label}">
|
|
81
|
+
<div class="nav-group-inner">${childLinks}</div>
|
|
76
82
|
</div>
|
|
77
|
-
${childLinks}
|
|
78
83
|
</div>
|
|
79
84
|
`
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
function initCollapsible(navEl, defaultExpandedKeys) {
|
|
88
|
+
const STORAGE_KEY = 'nav-collapsed-groups'
|
|
89
|
+
|
|
90
|
+
function getCollapsed() {
|
|
91
|
+
try {
|
|
92
|
+
return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'))
|
|
93
|
+
} catch {
|
|
94
|
+
return new Set()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function saveCollapsed(collapsed) {
|
|
99
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([...collapsed]))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (localStorage.getItem(STORAGE_KEY) === null) {
|
|
103
|
+
const allKeys = [...navEl.querySelectorAll('[data-group-toggle]')].map((b) => b.dataset.groupToggle)
|
|
104
|
+
saveCollapsed(new Set(allKeys.filter((k) => !defaultExpandedKeys.has(k))))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const collapsed = getCollapsed()
|
|
108
|
+
|
|
109
|
+
navEl.querySelectorAll('[data-group-toggle]').forEach((btn) => {
|
|
110
|
+
const key = btn.dataset.groupToggle
|
|
111
|
+
const children = navEl.querySelector(`[data-group-children="${key}"]`)
|
|
112
|
+
const chevron = btn.querySelector('.nav-group-chevron')
|
|
113
|
+
const hasActive = !!children?.querySelector('.text-white')
|
|
114
|
+
|
|
115
|
+
if (!hasActive && collapsed.has(key)) {
|
|
116
|
+
children?.classList.add('collapsed')
|
|
117
|
+
chevron?.classList.add('-rotate-90')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
btn.addEventListener('click', () => {
|
|
121
|
+
const nowCollapsed = children?.classList.toggle('collapsed')
|
|
122
|
+
chevron?.classList.toggle('-rotate-90', nowCollapsed)
|
|
123
|
+
|
|
124
|
+
const saved = getCollapsed()
|
|
125
|
+
if (nowCollapsed) saved.add(key)
|
|
126
|
+
else saved.delete(key)
|
|
127
|
+
saveCollapsed(saved)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
82
132
|
export function getManifest() {
|
|
83
133
|
return manifest
|
|
84
134
|
}
|
|
@@ -108,6 +158,11 @@ export function initNav() {
|
|
|
108
158
|
<div class="sidebar-scroll space-y-1">${links}</div>
|
|
109
159
|
</div>
|
|
110
160
|
`
|
|
161
|
+
const groups = NAV_ITEMS.filter((i) => i.children)
|
|
162
|
+
const defaultExpandedKeys = new Set()
|
|
163
|
+
if (groups[0]) defaultExpandedKeys.add(groups[0].label)
|
|
164
|
+
if (manifest.nav.assessmentSection?.label) defaultExpandedKeys.add(manifest.nav.assessmentSection.label)
|
|
165
|
+
initCollapsible(navEl, defaultExpandedKeys)
|
|
111
166
|
}
|
|
112
167
|
|
|
113
168
|
export function initMobileNav() {
|
package/src/js/theory.js
CHANGED
|
@@ -15,7 +15,6 @@ export async function initTheory(week) {
|
|
|
15
15
|
} catch {
|
|
16
16
|
container.innerHTML = `
|
|
17
17
|
<p class="text-red-600">Theorie voor week ${week} niet gevonden.</p>
|
|
18
|
-
<p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
|
|
19
18
|
`
|
|
20
19
|
return
|
|
21
20
|
}
|
|
@@ -23,7 +22,6 @@ export async function initTheory(week) {
|
|
|
23
22
|
if (!data?.html) {
|
|
24
23
|
container.innerHTML = `
|
|
25
24
|
<p class="text-amber-700">Theorie voor week ${week} is nog leeg.</p>
|
|
26
|
-
<p class="mt-2 text-sm text-zinc-500">Run <code class="font-mono">npm run generate-content</code> om content te genereren.</p>
|
|
27
25
|
`
|
|
28
26
|
return
|
|
29
27
|
}
|
package/templates/index.html
CHANGED
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-900 font-mono text-xs text-white">3</span>
|
|
71
71
|
|
|
72
72
|
<div>
|
|
73
|
-
<p class="font-medium text-zinc-900">
|
|
73
|
+
<p class="font-medium text-zinc-900">Meetmomenten toevoegen (optioneel)</p>
|
|
74
74
|
|
|
75
75
|
<p class="mt-1 text-sm text-zinc-500">
|
|
76
|
-
Plaats <code class="font-mono text-xs">content/
|
|
77
|
-
<code class="font-mono text-xs">practical-
|
|
76
|
+
Plaats <code class="font-mono text-xs">content/assessments/theory-assessment.md</code> en
|
|
77
|
+
<code class="font-mono text-xs">practical-assessment.md</code> met vragen in YAML frontmatter.
|
|
78
78
|
</p>
|
|
79
79
|
</div>
|
|
80
80
|
</li>
|
|
@@ -120,12 +120,20 @@ npm run preview # preview van dist/</code></pre>
|
|
|
120
120
|
</p>
|
|
121
121
|
</div>
|
|
122
122
|
|
|
123
|
-
<
|
|
123
|
+
<div class="mb-6 flex items-center justify-between">
|
|
124
|
+
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Curriculum</p>
|
|
125
|
+
|
|
126
|
+
<a href="e-module.pdf"
|
|
127
|
+
download
|
|
128
|
+
class="inline-flex items-center gap-1.5 rounded border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-600 hover:bg-zinc-50">
|
|
129
|
+
Download als PDF
|
|
130
|
+
</a>
|
|
131
|
+
</div>
|
|
124
132
|
|
|
125
133
|
<div class="mb-14 grid gap-px bg-zinc-200 sm:grid-cols-2"
|
|
126
134
|
data-home-curriculum></div>
|
|
127
135
|
|
|
128
|
-
<p class="mb-6 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
|
136
|
+
<p class="mb-6 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">Meetmoment</p>
|
|
129
137
|
|
|
130
138
|
<div class="grid gap-px bg-zinc-200 sm:grid-cols-3">
|
|
131
139
|
<a href="pages/checklist.html"
|
|
@@ -136,16 +144,16 @@ npm run preview # preview van dist/</code></pre>
|
|
|
136
144
|
<p class="mt-1 text-sm text-zinc-500">Houd je voortgang bij</p>
|
|
137
145
|
</a>
|
|
138
146
|
|
|
139
|
-
<a href="pages/
|
|
147
|
+
<a href="pages/meetmoment-theorie.html"
|
|
140
148
|
class="card-interactive block bg-white">
|
|
141
|
-
<h3 class="font-medium text-zinc-900">
|
|
149
|
+
<h3 class="font-medium text-zinc-900">Meetmoment theorie</h3>
|
|
142
150
|
|
|
143
151
|
<p class="mt-1 text-sm text-zinc-500">Meerkeuzevragen</p>
|
|
144
152
|
</a>
|
|
145
153
|
|
|
146
|
-
<a href="pages/
|
|
154
|
+
<a href="pages/meetmoment-praktijk.html"
|
|
147
155
|
class="card-interactive block bg-white">
|
|
148
|
-
<h3 class="font-medium text-zinc-900">
|
|
156
|
+
<h3 class="font-medium text-zinc-900">Meetmoment praktijk</h3>
|
|
149
157
|
|
|
150
158
|
<p class="mt-1 text-sm text-zinc-500">Praktijkvragen</p>
|
|
151
159
|
</a>
|