@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.

Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/assets/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2 +0 -0
  4. package/assets/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 +0 -0
  5. package/assets/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2 +0 -0
  6. package/assets/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
  7. package/assets/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
  8. package/assets/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
  9. package/package.json +92 -0
  10. package/scripts/check-course-coverage.mts +32 -0
  11. package/scripts/fix-static-image-extensions.mjs +78 -0
  12. package/scripts/generate-readme-toc.mts +32 -0
  13. package/scripts/resolve-course-paths.mjs +28 -0
  14. package/scripts/sync-images.mjs +88 -0
  15. package/src/components/AppShell/AppShell.module.css +40 -0
  16. package/src/components/AppShell/AppShell.tsx +135 -0
  17. package/src/components/AppShell/index.ts +1 -0
  18. package/src/components/Callout/Callout.module.css +68 -0
  19. package/src/components/Callout/Callout.tsx +83 -0
  20. package/src/components/Callout/index.ts +1 -0
  21. package/src/components/CodeBlock/CodeBlock.module.css +68 -0
  22. package/src/components/CodeBlock/CodeBlock.tsx +65 -0
  23. package/src/components/CodeBlock/index.ts +1 -0
  24. package/src/components/GateProvider/GateProvider.tsx +207 -0
  25. package/src/components/GateProvider/index.ts +1 -0
  26. package/src/components/Header/Breadcrumbs.tsx +50 -0
  27. package/src/components/Header/Header.module.css +131 -0
  28. package/src/components/Header/Header.tsx +26 -0
  29. package/src/components/Header/HeaderLessonNav.tsx +118 -0
  30. package/src/components/Header/index.ts +1 -0
  31. package/src/components/HomePage/HomePage.module.css +538 -0
  32. package/src/components/HomePage/HomePage.tsx +295 -0
  33. package/src/components/HomePage/index.ts +1 -0
  34. package/src/components/LessonAwareLink/LessonAwareLink.module.css +12 -0
  35. package/src/components/LessonAwareLink/LessonAwareLink.tsx +86 -0
  36. package/src/components/LessonAwareLink/index.ts +1 -0
  37. package/src/components/LessonLayout/LessonLayout.module.css +35 -0
  38. package/src/components/LessonLayout/LessonLayout.tsx +18 -0
  39. package/src/components/LessonLayout/index.ts +1 -0
  40. package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.module.css +367 -0
  41. package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.tsx +256 -0
  42. package/src/components/LessonLockedInterstitial/index.ts +1 -0
  43. package/src/components/LessonNav/LessonNav.module.css +84 -0
  44. package/src/components/LessonNav/LessonNav.tsx +64 -0
  45. package/src/components/LessonNav/index.ts +1 -0
  46. package/src/components/LessonPageLayout/LessonPageLayout.module.css +118 -0
  47. package/src/components/LessonPageLayout/LessonPageLayout.tsx +46 -0
  48. package/src/components/LessonPageLayout/index.ts +1 -0
  49. package/src/components/LessonSideMeta/LessonSideMeta.module.css +68 -0
  50. package/src/components/LessonSideMeta/LessonSideMeta.tsx +87 -0
  51. package/src/components/LessonSideMeta/index.ts +1 -0
  52. package/src/components/ModulePage/ModulePage.module.css +693 -0
  53. package/src/components/ModulePage/ModulePage.tsx +301 -0
  54. package/src/components/ModulePage/index.ts +1 -0
  55. package/src/components/ProgramDrawer/LockIcon.tsx +19 -0
  56. package/src/components/ProgramDrawer/ProgramDrawer.module.css +563 -0
  57. package/src/components/ProgramDrawer/ProgramDrawer.tsx +481 -0
  58. package/src/components/ProgramDrawer/index.ts +1 -0
  59. package/src/components/ProgressBar/ProgressBar.module.css +46 -0
  60. package/src/components/ProgressBar/ProgressBar.tsx +45 -0
  61. package/src/components/ProgressBar/index.ts +1 -0
  62. package/src/components/ProgressModeProvider/ProgressModeProvider.tsx +87 -0
  63. package/src/components/ProgressModeProvider/index.ts +1 -0
  64. package/src/components/ReadingPrefsProvider/ReadingPrefsProvider.tsx +100 -0
  65. package/src/components/ReadingPrefsProvider/index.ts +1 -0
  66. package/src/components/ReadingProgress/ReadingProgress.module.css +19 -0
  67. package/src/components/ReadingProgress/ReadingProgress.tsx +53 -0
  68. package/src/components/ReadingProgress/index.ts +1 -0
  69. package/src/components/SettingsToggle/SettingsToggle.module.css +888 -0
  70. package/src/components/SettingsToggle/SettingsToggle.tsx +688 -0
  71. package/src/components/SettingsToggle/index.ts +1 -0
  72. package/src/components/Sidebar/Sidebar.module.css +157 -0
  73. package/src/components/Sidebar/Sidebar.tsx +63 -0
  74. package/src/components/Sidebar/icons/GitHubIcon.tsx +17 -0
  75. package/src/components/Sidebar/icons/HomeIcon.tsx +22 -0
  76. package/src/components/Sidebar/icons/LanguageIcon.tsx +24 -0
  77. package/src/components/Sidebar/icons/ProgramIcon.tsx +23 -0
  78. package/src/components/Sidebar/icons/SettingsIcon.tsx +26 -0
  79. package/src/components/Sidebar/icons/ThemeIcon.tsx +22 -0
  80. package/src/components/Sidebar/icons/index.ts +6 -0
  81. package/src/components/Sidebar/index.ts +1 -0
  82. package/src/components/ThemeProvider/ThemeProvider.tsx +68 -0
  83. package/src/components/ThemeProvider/index.ts +1 -0
  84. package/src/components/Toc/Toc.module.css +78 -0
  85. package/src/components/Toc/Toc.tsx +92 -0
  86. package/src/components/Toc/index.ts +1 -0
  87. package/src/components/TranslationBanner/TranslationBanner.module.css +32 -0
  88. package/src/components/TranslationBanner/TranslationBanner.tsx +40 -0
  89. package/src/components/TranslationBanner/index.ts +1 -0
  90. package/src/config.d.mts +12 -0
  91. package/src/config.mjs +110 -0
  92. package/src/index.ts +62 -0
  93. package/src/layout/lang.tsx +44 -0
  94. package/src/layout/root.tsx +223 -0
  95. package/src/lib/course-loader.ts +33 -0
  96. package/src/lib/course.ts +429 -0
  97. package/src/lib/coverage.ts +141 -0
  98. package/src/lib/description.ts +43 -0
  99. package/src/lib/extract-toc.ts +59 -0
  100. package/src/lib/format.ts +55 -0
  101. package/src/lib/frontier-link.ts +37 -0
  102. package/src/lib/gate-init-script.ts +40 -0
  103. package/src/lib/gate-mark-script.ts +324 -0
  104. package/src/lib/i18n.ts +474 -0
  105. package/src/lib/lang.ts +90 -0
  106. package/src/lib/lesson-gate.ts +79 -0
  107. package/src/lib/lesson.ts +66 -0
  108. package/src/lib/markdown-components.tsx +51 -0
  109. package/src/lib/markdown.ts +180 -0
  110. package/src/lib/mdx-plugins/rehype-callout.ts +80 -0
  111. package/src/lib/mdx-plugins/remark-lesson-images.ts +109 -0
  112. package/src/lib/mdx-plugins/remark-link-rewrite.ts +231 -0
  113. package/src/lib/paths.ts +36 -0
  114. package/src/lib/program-drawer.ts +8 -0
  115. package/src/lib/progress-mode.ts +69 -0
  116. package/src/lib/progress.ts +182 -0
  117. package/src/lib/reading-prefs.ts +127 -0
  118. package/src/lib/readme-toc.ts +69 -0
  119. package/src/lib/site-url.ts +33 -0
  120. package/src/lib/sitemap.ts +112 -0
  121. package/src/lib/slug.ts +15 -0
  122. package/src/lib/theme.ts +78 -0
  123. package/src/lib/use-i18n.ts +25 -0
  124. package/src/og/icon.tsx +40 -0
  125. package/src/og/opengraph-image.tsx +126 -0
  126. package/src/pages/home.tsx +66 -0
  127. package/src/pages/lesson.tsx +260 -0
  128. package/src/pages/module.tsx +80 -0
  129. package/src/pages/not-found-lang.tsx +51 -0
  130. package/src/pages/not-found-root.tsx +48 -0
  131. package/src/pages/root.tsx +44 -0
  132. package/src/seo/robots.ts +16 -0
  133. package/src/seo/sitemap.ts +10 -0
  134. package/src/styles/globals.css +139 -0
  135. package/src/styles/markdown.css +265 -0
  136. package/src/styles/reset.css +89 -0
  137. 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
+ }