@dsbasko/cookbook-engine 0.1.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.
Potentially problematic release.
This version of @dsbasko/cookbook-engine might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
- package/package.json +92 -0
- package/scripts/check-course-coverage.mts +32 -0
- package/scripts/fix-static-image-extensions.mjs +78 -0
- package/scripts/generate-readme-toc.mts +32 -0
- package/scripts/resolve-course-paths.mjs +28 -0
- package/scripts/sync-images.mjs +88 -0
- package/src/components/AppShell/AppShell.module.css +40 -0
- package/src/components/AppShell/AppShell.tsx +135 -0
- package/src/components/AppShell/index.ts +1 -0
- package/src/components/Callout/Callout.module.css +68 -0
- package/src/components/Callout/Callout.tsx +83 -0
- package/src/components/Callout/index.ts +1 -0
- package/src/components/CodeBlock/CodeBlock.module.css +68 -0
- package/src/components/CodeBlock/CodeBlock.tsx +65 -0
- package/src/components/CodeBlock/index.ts +1 -0
- package/src/components/GateProvider/GateProvider.tsx +207 -0
- package/src/components/GateProvider/index.ts +1 -0
- package/src/components/Header/Breadcrumbs.tsx +50 -0
- package/src/components/Header/Header.module.css +131 -0
- package/src/components/Header/Header.tsx +26 -0
- package/src/components/Header/HeaderLessonNav.tsx +118 -0
- package/src/components/Header/index.ts +1 -0
- package/src/components/HomePage/HomePage.module.css +538 -0
- package/src/components/HomePage/HomePage.tsx +295 -0
- package/src/components/HomePage/index.ts +1 -0
- package/src/components/LessonAwareLink/LessonAwareLink.module.css +12 -0
- package/src/components/LessonAwareLink/LessonAwareLink.tsx +86 -0
- package/src/components/LessonAwareLink/index.ts +1 -0
- package/src/components/LessonLayout/LessonLayout.module.css +35 -0
- package/src/components/LessonLayout/LessonLayout.tsx +18 -0
- package/src/components/LessonLayout/index.ts +1 -0
- package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.module.css +367 -0
- package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.tsx +256 -0
- package/src/components/LessonLockedInterstitial/index.ts +1 -0
- package/src/components/LessonNav/LessonNav.module.css +84 -0
- package/src/components/LessonNav/LessonNav.tsx +64 -0
- package/src/components/LessonNav/index.ts +1 -0
- package/src/components/LessonPageLayout/LessonPageLayout.module.css +118 -0
- package/src/components/LessonPageLayout/LessonPageLayout.tsx +46 -0
- package/src/components/LessonPageLayout/index.ts +1 -0
- package/src/components/LessonSideMeta/LessonSideMeta.module.css +68 -0
- package/src/components/LessonSideMeta/LessonSideMeta.tsx +87 -0
- package/src/components/LessonSideMeta/index.ts +1 -0
- package/src/components/ModulePage/ModulePage.module.css +693 -0
- package/src/components/ModulePage/ModulePage.tsx +301 -0
- package/src/components/ModulePage/index.ts +1 -0
- package/src/components/ProgramDrawer/LockIcon.tsx +19 -0
- package/src/components/ProgramDrawer/ProgramDrawer.module.css +563 -0
- package/src/components/ProgramDrawer/ProgramDrawer.tsx +481 -0
- package/src/components/ProgramDrawer/index.ts +1 -0
- package/src/components/ProgressBar/ProgressBar.module.css +46 -0
- package/src/components/ProgressBar/ProgressBar.tsx +45 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/ProgressModeProvider/ProgressModeProvider.tsx +87 -0
- package/src/components/ProgressModeProvider/index.ts +1 -0
- package/src/components/ReadingPrefsProvider/ReadingPrefsProvider.tsx +100 -0
- package/src/components/ReadingPrefsProvider/index.ts +1 -0
- package/src/components/ReadingProgress/ReadingProgress.module.css +19 -0
- package/src/components/ReadingProgress/ReadingProgress.tsx +53 -0
- package/src/components/ReadingProgress/index.ts +1 -0
- package/src/components/SettingsToggle/SettingsToggle.module.css +888 -0
- package/src/components/SettingsToggle/SettingsToggle.tsx +688 -0
- package/src/components/SettingsToggle/index.ts +1 -0
- package/src/components/Sidebar/Sidebar.module.css +157 -0
- package/src/components/Sidebar/Sidebar.tsx +63 -0
- package/src/components/Sidebar/icons/GitHubIcon.tsx +17 -0
- package/src/components/Sidebar/icons/HomeIcon.tsx +22 -0
- package/src/components/Sidebar/icons/LanguageIcon.tsx +24 -0
- package/src/components/Sidebar/icons/ProgramIcon.tsx +23 -0
- package/src/components/Sidebar/icons/SettingsIcon.tsx +26 -0
- package/src/components/Sidebar/icons/ThemeIcon.tsx +22 -0
- package/src/components/Sidebar/icons/index.ts +6 -0
- package/src/components/Sidebar/index.ts +1 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +68 -0
- package/src/components/ThemeProvider/index.ts +1 -0
- package/src/components/Toc/Toc.module.css +78 -0
- package/src/components/Toc/Toc.tsx +92 -0
- package/src/components/Toc/index.ts +1 -0
- package/src/components/TranslationBanner/TranslationBanner.module.css +32 -0
- package/src/components/TranslationBanner/TranslationBanner.tsx +40 -0
- package/src/components/TranslationBanner/index.ts +1 -0
- package/src/config.d.mts +12 -0
- package/src/config.mjs +110 -0
- package/src/index.ts +62 -0
- package/src/layout/lang.tsx +44 -0
- package/src/layout/root.tsx +223 -0
- package/src/lib/course-loader.ts +33 -0
- package/src/lib/course.ts +429 -0
- package/src/lib/coverage.ts +141 -0
- package/src/lib/description.ts +43 -0
- package/src/lib/extract-toc.ts +59 -0
- package/src/lib/format.ts +55 -0
- package/src/lib/frontier-link.ts +37 -0
- package/src/lib/gate-init-script.ts +40 -0
- package/src/lib/gate-mark-script.ts +324 -0
- package/src/lib/i18n.ts +474 -0
- package/src/lib/lang.ts +90 -0
- package/src/lib/lesson-gate.ts +79 -0
- package/src/lib/lesson.ts +66 -0
- package/src/lib/markdown-components.tsx +51 -0
- package/src/lib/markdown.ts +180 -0
- package/src/lib/mdx-plugins/rehype-callout.ts +80 -0
- package/src/lib/mdx-plugins/remark-lesson-images.ts +109 -0
- package/src/lib/mdx-plugins/remark-link-rewrite.ts +231 -0
- package/src/lib/paths.ts +36 -0
- package/src/lib/program-drawer.ts +8 -0
- package/src/lib/progress-mode.ts +69 -0
- package/src/lib/progress.ts +182 -0
- package/src/lib/reading-prefs.ts +127 -0
- package/src/lib/readme-toc.ts +69 -0
- package/src/lib/site-url.ts +33 -0
- package/src/lib/sitemap.ts +112 -0
- package/src/lib/slug.ts +15 -0
- package/src/lib/theme.ts +78 -0
- package/src/lib/use-i18n.ts +25 -0
- package/src/og/icon.tsx +40 -0
- package/src/og/opengraph-image.tsx +126 -0
- package/src/pages/home.tsx +66 -0
- package/src/pages/lesson.tsx +260 -0
- package/src/pages/module.tsx +80 -0
- package/src/pages/not-found-lang.tsx +51 -0
- package/src/pages/not-found-root.tsx +48 -0
- package/src/pages/root.tsx +44 -0
- package/src/seo/robots.ts +16 -0
- package/src/seo/sitemap.ts +10 -0
- package/src/styles/globals.css +139 -0
- package/src/styles/markdown.css +265 -0
- package/src/styles/reset.css +89 -0
- package/src/styles/tokens.css +270 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { useGate } from '@/components/GateProvider';
|
|
7
|
+
import { LockIcon } from '@/components/ProgramDrawer/LockIcon';
|
|
8
|
+
import {
|
|
9
|
+
type Course,
|
|
10
|
+
type Module,
|
|
11
|
+
} from '@/lib/course';
|
|
12
|
+
import {
|
|
13
|
+
formatDurationHm,
|
|
14
|
+
formatDurationShort,
|
|
15
|
+
formatLessonCount,
|
|
16
|
+
parseDurationMin,
|
|
17
|
+
} from '@/lib/format';
|
|
18
|
+
import { lessonKey } from '@/lib/progress';
|
|
19
|
+
import { navigateToFrontierHref } from '@/lib/frontier-link';
|
|
20
|
+
import { useLang, useT } from '@/lib/use-i18n';
|
|
21
|
+
import styles from './ModulePage.module.css';
|
|
22
|
+
|
|
23
|
+
type ModulePageProps = {
|
|
24
|
+
course: Course;
|
|
25
|
+
module: Module;
|
|
26
|
+
level: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function ModulePage({ course, module, level }: ModulePageProps) {
|
|
30
|
+
const moduleIndex = course.modules.findIndex((m) => m.id === module.id);
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
const { basePath } = useGate();
|
|
33
|
+
const t = useT();
|
|
34
|
+
const lang = useLang();
|
|
35
|
+
const prevModule = moduleIndex > 0 ? course.modules[moduleIndex - 1] : null;
|
|
36
|
+
const nextModule =
|
|
37
|
+
moduleIndex >= 0 && moduleIndex < course.modules.length - 1
|
|
38
|
+
? course.modules[moduleIndex + 1]
|
|
39
|
+
: null;
|
|
40
|
+
|
|
41
|
+
const moduleDurationMin = useMemo(
|
|
42
|
+
() => module.lessons.reduce((s, l) => s + parseDurationMin(l.duration), 0),
|
|
43
|
+
[module],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const totalLessons = module.lessons.length;
|
|
47
|
+
|
|
48
|
+
// CSV of this module's lesson keys, attached to the side card and CTA row
|
|
49
|
+
// so the gate-paint inline script can compute per-module progress without
|
|
50
|
+
// re-deriving the module shape on its own.
|
|
51
|
+
const moduleKeysCsv = useMemo(
|
|
52
|
+
() => module.lessons.map((l) => lessonKey(module.id, l.slug)).join(','),
|
|
53
|
+
[module],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// SSR + pre-hydration baseline: pretend nothing has been done yet. The
|
|
57
|
+
// gate-paint script reads localStorage and rewrites textContent / sets
|
|
58
|
+
// data-* attributes before first paint, so the user never sees this 0%
|
|
59
|
+
// state flash to the real one. All React state derived from `progress`
|
|
60
|
+
// has been removed from this component — it would only re-derive the
|
|
61
|
+
// same numbers and cause a hydration re-render.
|
|
62
|
+
const firstLesson = module.lessons[0] ?? null;
|
|
63
|
+
const fallbackHref = firstLesson
|
|
64
|
+
? `/${lang}/${module.id}/${firstLesson.slug}`
|
|
65
|
+
: '#';
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={styles.page}>
|
|
69
|
+
<section className={styles.hero}>
|
|
70
|
+
<div className={styles.heroText}>
|
|
71
|
+
<div className={styles.eyebrow}>
|
|
72
|
+
<span className={styles.eyebrowNum}>
|
|
73
|
+
{String(moduleIndex + 1).padStart(2, '0')}
|
|
74
|
+
</span>
|
|
75
|
+
<span className={styles.eyebrowOf}>
|
|
76
|
+
/ {String(course.modules.length).padStart(2, '0')}
|
|
77
|
+
</span>
|
|
78
|
+
<span className={styles.eyebrowDot}>·</span>
|
|
79
|
+
<span>
|
|
80
|
+
{formatLessonCount(totalLessons, lang)}
|
|
81
|
+
</span>
|
|
82
|
+
<span className={styles.eyebrowDot}>·</span>
|
|
83
|
+
<span>{formatDurationHm(moduleDurationMin, lang)}</span>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<h1 className={styles.title}>{module.title}</h1>
|
|
87
|
+
<p className={styles.desc}>{collapseWhitespace(module.description)}</p>
|
|
88
|
+
|
|
89
|
+
<div
|
|
90
|
+
className={styles.ctaRow}
|
|
91
|
+
data-cta-frontier="module"
|
|
92
|
+
data-cta-state="not-started"
|
|
93
|
+
data-progress-keys={moduleKeysCsv}
|
|
94
|
+
suppressHydrationWarning
|
|
95
|
+
>
|
|
96
|
+
{/* Three CTA variants stacked in the DOM, exactly one visible per
|
|
97
|
+
module state. The gate-paint script flips data-cta-state and
|
|
98
|
+
rewrites href + title on the in-progress variant; CSS hides
|
|
99
|
+
the other two. JSX never re-renders this region in response
|
|
100
|
+
to progress changes — no flash. */}
|
|
101
|
+
<Link
|
|
102
|
+
href={fallbackHref}
|
|
103
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
104
|
+
data-cta-variant="not-started"
|
|
105
|
+
>
|
|
106
|
+
{t.startModule}
|
|
107
|
+
<span className={styles.btnArrow}>→</span>
|
|
108
|
+
</Link>
|
|
109
|
+
<Link
|
|
110
|
+
href={fallbackHref}
|
|
111
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
112
|
+
data-cta-variant="in-progress"
|
|
113
|
+
data-cta-frontier-link
|
|
114
|
+
suppressHydrationWarning
|
|
115
|
+
onClick={(e) => navigateToFrontierHref(e, router, basePath)}
|
|
116
|
+
>
|
|
117
|
+
{t.continueModulePrefix} ·{' '}
|
|
118
|
+
<span data-cta-frontier-title suppressHydrationWarning>
|
|
119
|
+
{firstLesson?.title ?? ''}
|
|
120
|
+
</span>
|
|
121
|
+
<span className={styles.btnArrow}>→</span>
|
|
122
|
+
</Link>
|
|
123
|
+
<Link
|
|
124
|
+
href={fallbackHref}
|
|
125
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
126
|
+
data-cta-variant="complete"
|
|
127
|
+
>
|
|
128
|
+
{t.rereadModule}
|
|
129
|
+
<span className={styles.btnArrow}>→</span>
|
|
130
|
+
</Link>
|
|
131
|
+
{nextModule && (
|
|
132
|
+
<Link href={`/${lang}/${nextModule.id}`} className={`${styles.btn} ${styles.btnGhost}`}>
|
|
133
|
+
{t.nextModule} <span className={styles.btnArrow}>→</span>
|
|
134
|
+
</Link>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<aside
|
|
140
|
+
className={styles.sideCard}
|
|
141
|
+
aria-label={t.moduleProgress}
|
|
142
|
+
data-progress-scope="module"
|
|
143
|
+
data-progress-keys={moduleKeysCsv}
|
|
144
|
+
data-progress-state="not-started"
|
|
145
|
+
suppressHydrationWarning
|
|
146
|
+
>
|
|
147
|
+
<div className={styles.sideRow}>
|
|
148
|
+
<span className={styles.sideLabel}>{t.progress}</span>
|
|
149
|
+
<span className={styles.sideVal}>
|
|
150
|
+
<span data-progress-count suppressHydrationWarning>
|
|
151
|
+
0
|
|
152
|
+
</span>{' '}
|
|
153
|
+
/ {totalLessons}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div className={styles.sideBar} aria-hidden="true">
|
|
157
|
+
<span
|
|
158
|
+
className={styles.sideFill}
|
|
159
|
+
data-progress-bar
|
|
160
|
+
style={{ width: '0%' }}
|
|
161
|
+
suppressHydrationWarning
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
<div className={styles.sidePct}>
|
|
165
|
+
<span data-progress-pct suppressHydrationWarning>
|
|
166
|
+
0
|
|
167
|
+
</span>
|
|
168
|
+
%
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className={styles.sideDivider} />
|
|
172
|
+
|
|
173
|
+
<dl className={styles.sideMeta}>
|
|
174
|
+
<div>
|
|
175
|
+
<dt className={styles.sideMetaLabel}>{t.lessonsCount}</dt>
|
|
176
|
+
<dd className={styles.sideMetaValue}>{totalLessons}</dd>
|
|
177
|
+
</div>
|
|
178
|
+
<div>
|
|
179
|
+
<dt className={styles.sideMetaLabel}>{t.durationLabelShort}</dt>
|
|
180
|
+
<dd className={styles.sideMetaValue}>{formatDurationHm(moduleDurationMin, lang)}</dd>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<dt className={styles.sideMetaLabel}>{t.stackLabelShort}</dt>
|
|
184
|
+
<dd className={styles.sideMetaValue}>{level}</dd>
|
|
185
|
+
</div>
|
|
186
|
+
</dl>
|
|
187
|
+
</aside>
|
|
188
|
+
</section>
|
|
189
|
+
|
|
190
|
+
<header className={styles.sectionHead}>
|
|
191
|
+
<div>
|
|
192
|
+
<div className={styles.sectionEyebrow}>/ lessons</div>
|
|
193
|
+
<h2 className={styles.sectionTitle}>{t.moduleLessonsHeading}</h2>
|
|
194
|
+
</div>
|
|
195
|
+
<div className={styles.sectionTools}>
|
|
196
|
+
{formatLessonCount(totalLessons, lang)} ·{' '}
|
|
197
|
+
{formatDurationHm(moduleDurationMin, lang)}
|
|
198
|
+
</div>
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
<ol className={styles.lessons} data-lesson-group={module.id}>
|
|
202
|
+
{module.lessons.map((lesson, index) => {
|
|
203
|
+
const key = lessonKey(module.id, lesson.slug);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<li key={lesson.slug} className={styles.lessonItem}>
|
|
207
|
+
<Link
|
|
208
|
+
href={`/${lang}/${module.id}/${lesson.slug}`}
|
|
209
|
+
className={styles.lessonRow}
|
|
210
|
+
data-lesson-key={key}
|
|
211
|
+
onClick={(e) => {
|
|
212
|
+
if (e.currentTarget.getAttribute('data-locked') === 'true') {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
title={t.lessonLockShort}
|
|
217
|
+
// Gate-mark inline script flips data-completed / data-next /
|
|
218
|
+
// data-locked / aria-disabled / tabindex on every [data-lesson-key]
|
|
219
|
+
// before hydration. React's reconciliation would otherwise warn
|
|
220
|
+
// about "extra attributes from the server" since the VDOM has none.
|
|
221
|
+
suppressHydrationWarning
|
|
222
|
+
>
|
|
223
|
+
<span className={styles.lessonNum}>
|
|
224
|
+
{String(index + 1).padStart(2, '0')}
|
|
225
|
+
</span>
|
|
226
|
+
{/* All four status glyphs are present in the DOM at all times.
|
|
227
|
+
CSS shows exactly one based on data-completed / data-next /
|
|
228
|
+
data-locked, which the gate-paint script flips synchronously
|
|
229
|
+
before paint and again on progress changes. */}
|
|
230
|
+
<span className={styles.lessonStatus} aria-hidden="true">
|
|
231
|
+
<span
|
|
232
|
+
className={`${styles.lessonCircle} ${styles.statusDefault}`}
|
|
233
|
+
/>
|
|
234
|
+
<span className={`${styles.lessonDot} ${styles.statusNext}`} />
|
|
235
|
+
<span className={`${styles.lessonCheck} ${styles.statusDone}`}>
|
|
236
|
+
✓
|
|
237
|
+
</span>
|
|
238
|
+
<span
|
|
239
|
+
className={`${styles.lessonLockSlot} ${styles.statusLocked}`}
|
|
240
|
+
>
|
|
241
|
+
<LockIcon />
|
|
242
|
+
</span>
|
|
243
|
+
</span>
|
|
244
|
+
<span className={styles.lessonText}>
|
|
245
|
+
<span className={styles.lessonTitle}>{lesson.title}</span>
|
|
246
|
+
{/* Hint is always present in DOM; CSS shows it only when
|
|
247
|
+
the row carries data-next and not data-locked. */}
|
|
248
|
+
<span className={styles.lessonHint}>{t.lessonHintContinue}</span>
|
|
249
|
+
</span>
|
|
250
|
+
{lesson.tags && lesson.tags.length > 0 && (
|
|
251
|
+
<span className={styles.lessonTags}>
|
|
252
|
+
{lesson.tags.slice(0, 3).map((tag) => (
|
|
253
|
+
<span key={tag} className={styles.lessonTag}>
|
|
254
|
+
#{tag}
|
|
255
|
+
</span>
|
|
256
|
+
))}
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
<span className={styles.lessonDuration}>
|
|
260
|
+
{formatDurationShort(parseDurationMin(lesson.duration), lang)}
|
|
261
|
+
</span>
|
|
262
|
+
<span className={styles.lessonArrow} aria-hidden="true">
|
|
263
|
+
→
|
|
264
|
+
</span>
|
|
265
|
+
</Link>
|
|
266
|
+
</li>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
269
|
+
</ol>
|
|
270
|
+
|
|
271
|
+
<nav className={styles.moduleNav} aria-label={t.lessonNeighbourModulesLabel}>
|
|
272
|
+
{prevModule ? (
|
|
273
|
+
<Link
|
|
274
|
+
href={`/${lang}/${prevModule.id}`}
|
|
275
|
+
className={`${styles.navCard} ${styles.navCardPrev}`}
|
|
276
|
+
>
|
|
277
|
+
<span className={styles.navLabel}>{t.prevModule}</span>
|
|
278
|
+
<span className={styles.navTitle}>{prevModule.title}</span>
|
|
279
|
+
</Link>
|
|
280
|
+
) : (
|
|
281
|
+
<span className={`${styles.navCard} ${styles.navCardDisabled}`} aria-hidden="true" />
|
|
282
|
+
)}
|
|
283
|
+
{nextModule ? (
|
|
284
|
+
<Link
|
|
285
|
+
href={`/${lang}/${nextModule.id}`}
|
|
286
|
+
className={`${styles.navCard} ${styles.navCardNext}`}
|
|
287
|
+
>
|
|
288
|
+
<span className={styles.navLabel}>{t.nextModule} →</span>
|
|
289
|
+
<span className={styles.navTitle}>{nextModule.title}</span>
|
|
290
|
+
</Link>
|
|
291
|
+
) : (
|
|
292
|
+
<span className={`${styles.navCard} ${styles.navCardDisabled}`} aria-hidden="true" />
|
|
293
|
+
)}
|
|
294
|
+
</nav>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function collapseWhitespace(text: string): string {
|
|
300
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ModulePage } from './ModulePage';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function LockIcon() {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
width="12"
|
|
5
|
+
height="12"
|
|
6
|
+
viewBox="0 0 24 24"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth="2"
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
focusable="false"
|
|
14
|
+
>
|
|
15
|
+
<rect x="4" y="11" width="16" height="10" rx="2" />
|
|
16
|
+
<path d="M8 11V7a4 4 0 0 1 8 0v4" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|