@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,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useGate } from '@/components/GateProvider';
|
|
5
|
+
import { useProgressMode } from '@/components/ProgressModeProvider';
|
|
6
|
+
import { lessonKey, markCompletedAndAdvance } from '@/lib/progress';
|
|
7
|
+
import { useLang, useT } from '@/lib/use-i18n';
|
|
8
|
+
import styles from './LessonNav.module.css';
|
|
9
|
+
|
|
10
|
+
export type LessonNavLink = {
|
|
11
|
+
moduleId: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
title: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type LessonNavProps = {
|
|
17
|
+
prev: LessonNavLink | null;
|
|
18
|
+
next: LessonNavLink | null;
|
|
19
|
+
currentModuleId: string;
|
|
20
|
+
currentSlug: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function LessonNav({ prev, next, currentModuleId, currentSlug }: LessonNavProps) {
|
|
24
|
+
const gate = useGate();
|
|
25
|
+
const { disabled } = useProgressMode();
|
|
26
|
+
const t = useT();
|
|
27
|
+
const lang = useLang();
|
|
28
|
+
const handleNextClick = () => {
|
|
29
|
+
// Free-reading mode: the link only navigates, it must not record progress.
|
|
30
|
+
if (disabled) return;
|
|
31
|
+
// Single entry point: marks completed, advances the sticky furthest
|
|
32
|
+
// pointer, dispatches the change event. Keeps gate state internally
|
|
33
|
+
// consistent so an in-flight read in another tab doesn't see a half-write.
|
|
34
|
+
markCompletedAndAdvance(gate.course, lessonKey(currentModuleId, currentSlug));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<nav className={styles.row} aria-label={t.lessonNavLabel}>
|
|
39
|
+
{prev ? (
|
|
40
|
+
<Link
|
|
41
|
+
href={`/${lang}/${prev.moduleId}/${prev.slug}`}
|
|
42
|
+
className={`${styles.card} ${styles.prev}`}
|
|
43
|
+
>
|
|
44
|
+
<span className={styles.label}>{t.prevLesson}</span>
|
|
45
|
+
<span className={styles.title}>{prev.title}</span>
|
|
46
|
+
</Link>
|
|
47
|
+
) : (
|
|
48
|
+
<span className={styles.placeholder} aria-hidden="true" />
|
|
49
|
+
)}
|
|
50
|
+
{next ? (
|
|
51
|
+
<Link
|
|
52
|
+
href={`/${lang}/${next.moduleId}/${next.slug}`}
|
|
53
|
+
className={`${styles.card} ${styles.next}`}
|
|
54
|
+
onClick={handleNextClick}
|
|
55
|
+
>
|
|
56
|
+
<span className={styles.label}>{t.nextLesson}</span>
|
|
57
|
+
<span className={styles.title}>{next.title}</span>
|
|
58
|
+
</Link>
|
|
59
|
+
) : (
|
|
60
|
+
<span className={styles.placeholder} aria-hidden="true" />
|
|
61
|
+
)}
|
|
62
|
+
</nav>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LessonNav, type LessonNavLink } from './LessonNav';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: minmax(0, 1fr) 260px;
|
|
4
|
+
gap: 56px;
|
|
5
|
+
align-items: start;
|
|
6
|
+
padding: 56px var(--space-8) 96px;
|
|
7
|
+
max-width: calc(var(--prose-max-width) + 260px + 56px + var(--space-8) * 2);
|
|
8
|
+
width: 100%;
|
|
9
|
+
margin: 0 auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.page[data-has-side='false'] {
|
|
13
|
+
grid-template-columns: minmax(0, 1fr);
|
|
14
|
+
max-width: calc(var(--prose-max-width) + var(--space-8) * 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.content {
|
|
18
|
+
min-width: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.article {
|
|
22
|
+
min-width: 0;
|
|
23
|
+
max-width: var(--prose-max-width);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hero {
|
|
27
|
+
padding-bottom: var(--space-8);
|
|
28
|
+
margin-bottom: var(--space-8);
|
|
29
|
+
border-bottom: 1px solid var(--bg-stroke);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.title {
|
|
33
|
+
font-family: var(--prose-font-active), Georgia, 'Source Serif Pro', serif;
|
|
34
|
+
font-size: calc(var(--prose-font-size) * 4.5);
|
|
35
|
+
line-height: 1;
|
|
36
|
+
letter-spacing: -0.03em;
|
|
37
|
+
font-weight: var(--font-weight-bold);
|
|
38
|
+
margin: 0;
|
|
39
|
+
text-wrap: balance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.prose {
|
|
43
|
+
font-family: var(--prose-font-active), Georgia, 'Source Serif Pro', serif;
|
|
44
|
+
font-size: var(--prose-font-size);
|
|
45
|
+
line-height: 1.7;
|
|
46
|
+
color: var(--content-primary);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Apply serif sizing to markdown body without rewriting markdown.css. */
|
|
50
|
+
.prose :global(.markdown) {
|
|
51
|
+
font-family: inherit;
|
|
52
|
+
font-size: inherit;
|
|
53
|
+
line-height: inherit;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Headings inherit the prose font family so the reading-preferences toggle
|
|
57
|
+
* (serif/sans/lora) updates body copy and headings together. */
|
|
58
|
+
.prose :global(.markdown h2) {
|
|
59
|
+
letter-spacing: -0.02em;
|
|
60
|
+
margin-top: 40px;
|
|
61
|
+
margin-bottom: var(--space-5);
|
|
62
|
+
scroll-margin-top: calc(56px + var(--space-4));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.prose :global(.markdown p) {
|
|
66
|
+
text-wrap: pretty;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.footer {
|
|
70
|
+
margin-top: 56px;
|
|
71
|
+
padding-top: var(--space-8);
|
|
72
|
+
border-top: 1px solid var(--bg-stroke);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Right sidebar: sticks within its own grid column. */
|
|
76
|
+
.side {
|
|
77
|
+
position: sticky;
|
|
78
|
+
top: calc(56px + var(--space-5));
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--space-8);
|
|
82
|
+
padding-top: var(--space-2);
|
|
83
|
+
max-height: calc(100dvh - 56px - var(--space-8));
|
|
84
|
+
overflow-y: auto;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (max-width: 1100px) {
|
|
88
|
+
.page {
|
|
89
|
+
grid-template-columns: minmax(0, 1fr);
|
|
90
|
+
gap: var(--space-8);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.side {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.title {
|
|
98
|
+
font-size: calc(var(--prose-font-size) * 3.5);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media (max-width: 720px) {
|
|
103
|
+
.page {
|
|
104
|
+
padding: var(--space-6) var(--space-5) var(--space-10);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.title {
|
|
108
|
+
font-size: calc(var(--prose-font-size) * 2.4);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.prose {
|
|
112
|
+
line-height: 1.65;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.prose :global(.markdown h2) {
|
|
116
|
+
margin-top: var(--space-8);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { getDict } from '@/lib/i18n';
|
|
3
|
+
import { type Lang } from '@/lib/lang';
|
|
4
|
+
import styles from './LessonPageLayout.module.css';
|
|
5
|
+
|
|
6
|
+
type LessonPageLayoutProps = {
|
|
7
|
+
lang: Lang;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
tocSlot?: ReactNode;
|
|
12
|
+
sideMetaSlot?: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function LessonPageLayout({
|
|
16
|
+
lang,
|
|
17
|
+
title,
|
|
18
|
+
children,
|
|
19
|
+
footer,
|
|
20
|
+
tocSlot,
|
|
21
|
+
sideMetaSlot,
|
|
22
|
+
}: LessonPageLayoutProps) {
|
|
23
|
+
const hasSide = Boolean(tocSlot || sideMetaSlot);
|
|
24
|
+
const t = getDict(lang);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={styles.page} data-has-side={hasSide ? 'true' : 'false'}>
|
|
28
|
+
<div className={styles.content}>
|
|
29
|
+
<article className={styles.article} data-reading-target="">
|
|
30
|
+
<header className={styles.hero}>
|
|
31
|
+
<h1 className={styles.title}>{title}</h1>
|
|
32
|
+
</header>
|
|
33
|
+
<div className={styles.prose}>{children}</div>
|
|
34
|
+
</article>
|
|
35
|
+
{footer && <footer className={styles.footer}>{footer}</footer>}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{hasSide && (
|
|
39
|
+
<aside className={styles.side} aria-label={t.lessonInfoLabel}>
|
|
40
|
+
{tocSlot}
|
|
41
|
+
{sideMetaSlot}
|
|
42
|
+
</aside>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LessonPageLayout } from './LessonPageLayout';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
.meta {
|
|
2
|
+
border-top: 1px solid var(--bg-stroke);
|
|
3
|
+
padding-top: var(--space-5);
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--space-3);
|
|
7
|
+
font-size: 13px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.row {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: 2px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.key {
|
|
17
|
+
font-family: var(--font-mono), ui-monospace, monospace;
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
color: var(--content-tertiary);
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
letter-spacing: 0.06em;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.value {
|
|
25
|
+
color: var(--content-primary);
|
|
26
|
+
text-decoration: none;
|
|
27
|
+
font-weight: var(--font-weight-medium);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
a.value:hover {
|
|
31
|
+
color: var(--accent-main);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.tags {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
gap: 4px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.tag {
|
|
41
|
+
font-family: var(--font-mono), ui-monospace, monospace;
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
padding: 1px 6px;
|
|
44
|
+
background: var(--bg-surface);
|
|
45
|
+
border: 1px solid var(--bg-stroke);
|
|
46
|
+
border-radius: var(--radius-sm);
|
|
47
|
+
color: var(--content-secondary);
|
|
48
|
+
font-weight: var(--font-weight-regular);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.markButton {
|
|
52
|
+
composes: btn btnPrimary from '../HomePage/HomePage.module.css';
|
|
53
|
+
margin-top: var(--space-3);
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The "Пометить непрочитанным" button is always in the DOM (so SSR markup
|
|
59
|
+
matches every hydration), but only visible when the wrapper carries
|
|
60
|
+
data-completed='true'. The gate-paint inline script stamps that
|
|
61
|
+
attribute synchronously before first paint based on localStorage. */
|
|
62
|
+
.meta [data-show-when-completed] {
|
|
63
|
+
display: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.meta[data-completed='true'] [data-show-when-completed] {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useProgressMode } from '@/components/ProgressModeProvider';
|
|
5
|
+
import {
|
|
6
|
+
lessonKey,
|
|
7
|
+
PROGRESS_CHANGE_EVENT,
|
|
8
|
+
unmarkCompleted,
|
|
9
|
+
} from '@/lib/progress';
|
|
10
|
+
import { useLang, useT } from '@/lib/use-i18n';
|
|
11
|
+
import styles from './LessonSideMeta.module.css';
|
|
12
|
+
|
|
13
|
+
type LessonSideMetaProps = {
|
|
14
|
+
moduleId: string;
|
|
15
|
+
moduleTitle: string;
|
|
16
|
+
moduleIndex: number;
|
|
17
|
+
slug: string;
|
|
18
|
+
duration: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function LessonSideMeta({
|
|
23
|
+
moduleId,
|
|
24
|
+
moduleTitle,
|
|
25
|
+
moduleIndex,
|
|
26
|
+
slug,
|
|
27
|
+
duration,
|
|
28
|
+
tags,
|
|
29
|
+
}: LessonSideMetaProps) {
|
|
30
|
+
const key = lessonKey(moduleId, slug);
|
|
31
|
+
const moduleNum = String(moduleIndex).padStart(2, '0');
|
|
32
|
+
const t = useT();
|
|
33
|
+
const lang = useLang();
|
|
34
|
+
const { disabled } = useProgressMode();
|
|
35
|
+
|
|
36
|
+
const handleUnmark = () => {
|
|
37
|
+
// Defence in depth: in free-reading mode the GateProvider strips
|
|
38
|
+
// `data-completed`, so CSS already hides this button. Guard the write too in
|
|
39
|
+
// case the handler is reached directly — free-reading mode must never touch
|
|
40
|
+
// progress storage.
|
|
41
|
+
if (disabled) return;
|
|
42
|
+
unmarkCompleted(key);
|
|
43
|
+
window.dispatchEvent(new Event(PROGRESS_CHANGE_EVENT));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={styles.meta}
|
|
49
|
+
data-lesson-key={key}
|
|
50
|
+
// Gate-mark inline script flips data-completed / data-locked on every
|
|
51
|
+
// [data-lesson-key] before hydration. Without this hint React warns
|
|
52
|
+
// about "extra attributes from the server" on this div.
|
|
53
|
+
suppressHydrationWarning
|
|
54
|
+
>
|
|
55
|
+
<div className={styles.row}>
|
|
56
|
+
<span className={styles.key}>{t.moduleMetaKey}</span>
|
|
57
|
+
<Link href={`/${lang}/${moduleId}`} className={styles.value}>
|
|
58
|
+
{moduleNum} · {moduleTitle}
|
|
59
|
+
</Link>
|
|
60
|
+
</div>
|
|
61
|
+
<div className={styles.row}>
|
|
62
|
+
<span className={styles.key}>{t.readingTimeMetaKey}</span>
|
|
63
|
+
<span className={styles.value}>{duration}</span>
|
|
64
|
+
</div>
|
|
65
|
+
{tags.length > 0 && (
|
|
66
|
+
<div className={styles.row}>
|
|
67
|
+
<span className={styles.key}>{t.tagsMetaKey}</span>
|
|
68
|
+
<span className={styles.tags}>
|
|
69
|
+
{tags.map((tag) => (
|
|
70
|
+
<span key={tag} className={styles.tag}>
|
|
71
|
+
#{tag}
|
|
72
|
+
</span>
|
|
73
|
+
))}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
className={styles.markButton}
|
|
80
|
+
data-show-when-completed
|
|
81
|
+
onClick={handleUnmark}
|
|
82
|
+
>
|
|
83
|
+
{t.markUnread}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LessonSideMeta } from './LessonSideMeta';
|