Skip to content

Commit bfc9c87

Browse files
committed
Implement chapter pages into the UI
1 parent a0e109e commit bfc9c87

9 files changed

Lines changed: 913 additions & 27 deletions

File tree

‎ui/parse_lessons.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ def humanize_name(name: str) -> str:
3333
}
3434

3535

36+
def extract_docstring_fallback(content: str) -> str:
37+
content = content.strip()
38+
if content.startswith(('"""', "'''")):
39+
quote = content[:3]
40+
end_idx = content.find(quote, 3)
41+
if end_idx != -1:
42+
return content[3:end_idx].strip()
43+
return ""
44+
45+
3646
def parse_file(filepath: str, relative_path: str) -> dict:
3747
with open(filepath, encoding="utf-8") as f:
3848
content = f.read()
@@ -41,7 +51,7 @@ def parse_file(filepath: str, relative_path: str) -> dict:
4151
tree = ast.parse(content)
4252
docstring = ast.get_docstring(tree) or ""
4353
except Exception:
44-
docstring = ""
54+
docstring = extract_docstring_fallback(content)
4555

4656
category = relative_path.split(os.sep)[0]
4757
filename = os.path.basename(filepath)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
import { ChevronLeft, ChevronRight } from '@lucide/astro';
3+
import { useTranslations, type languages } from '../../i18n/ui';
4+
5+
interface ChapterItem {
6+
id: string;
7+
name: string;
8+
}
9+
10+
interface Props {
11+
prevChapter?: ChapterItem | null;
12+
nextChapter?: ChapterItem | null;
13+
isFirstChapter?: boolean;
14+
activeLang?: keyof typeof languages;
15+
}
16+
17+
const {
18+
prevChapter,
19+
nextChapter,
20+
isFirstChapter = false,
21+
activeLang = 'en',
22+
} = Astro.props;
23+
24+
const t = useTranslations(activeLang);
25+
---
26+
27+
<nav class="chapter-nav" aria-label="Chapter navigation">
28+
{isFirstChapter ? (
29+
<a href={`/ultimate-python/${activeLang}/`} class="chapter-nav-btn prev">
30+
<span class="nav-direction">
31+
<ChevronLeft size={14} />
32+
<span>{t('nav.home')}</span>
33+
</span>
34+
<span class="nav-title">{t('site.title')}</span>
35+
</a>
36+
) : prevChapter ? (
37+
<a
38+
href={`/ultimate-python/${activeLang}/${prevChapter.id}/`}
39+
class="chapter-nav-btn prev"
40+
>
41+
<span class="nav-direction">
42+
<ChevronLeft size={14} />
43+
<span>{t('chapter.prev')}</span>
44+
</span>
45+
<span class="nav-title">{prevChapter.name}</span>
46+
</a>
47+
) : (
48+
<div class="nav-spacer" />
49+
)}
50+
51+
{nextChapter ? (
52+
<a
53+
href={`/ultimate-python/${activeLang}/${nextChapter.id}/`}
54+
class="chapter-nav-btn next"
55+
>
56+
<span class="nav-direction">
57+
<span>{t('chapter.next')}</span>
58+
<ChevronRight size={14} />
59+
</span>
60+
<span class="nav-title">{nextChapter.name}</span>
61+
</a>
62+
) : (
63+
<div class="nav-spacer" />
64+
)}
65+
</nav>
66+
67+
<style>
68+
.chapter-nav {
69+
display: flex;
70+
justify-content: space-between;
71+
gap: 1.5rem;
72+
padding-top: 2.5rem;
73+
border-top: 1px solid var(--border-color);
74+
margin-top: 3.5rem;
75+
width: 100%;
76+
}
77+
78+
.chapter-nav-btn {
79+
display: flex;
80+
flex-direction: column;
81+
padding: 1rem 1.35rem;
82+
border-radius: 8px;
83+
background-color: var(--bg-secondary);
84+
border: 1px solid var(--border-color);
85+
color: var(--text-primary);
86+
transition:
87+
transform var(--transition-speed),
88+
border-color var(--transition-speed),
89+
box-shadow var(--transition-speed);
90+
min-width: 180px;
91+
max-width: 48%;
92+
text-decoration: none;
93+
}
94+
95+
.chapter-nav-btn:hover {
96+
background-color: var(--bg-tertiary);
97+
border-color: var(--forest-green);
98+
transform: translateY(-2px);
99+
box-shadow: 0 4px 12px rgba(27, 67, 50, 0.08);
100+
}
101+
102+
.chapter-nav-btn.next {
103+
margin-left: auto;
104+
align-items: flex-end;
105+
text-align: right;
106+
}
107+
108+
.nav-direction {
109+
font-size: 0.75rem;
110+
color: var(--text-secondary);
111+
text-transform: uppercase;
112+
letter-spacing: 0.05em;
113+
font-family: var(--font-sans);
114+
margin-bottom: 0.35rem;
115+
display: inline-flex;
116+
align-items: center;
117+
gap: 0.35rem;
118+
font-weight: 500;
119+
}
120+
121+
.chapter-nav-btn:hover .nav-direction {
122+
color: var(--forest-green);
123+
}
124+
125+
.nav-title {
126+
font-size: 1.05rem;
127+
font-weight: 600;
128+
color: var(--forest-green);
129+
font-family: var(--font-serif);
130+
white-space: nowrap;
131+
overflow: hidden;
132+
text-overflow: ellipsis;
133+
max-width: 100%;
134+
}
135+
136+
.nav-spacer {
137+
flex: 1;
138+
}
139+
140+
@media (width <= 640px) {
141+
.chapter-nav {
142+
flex-direction: column;
143+
gap: 1rem;
144+
padding-top: 1.5rem;
145+
margin-top: 2rem;
146+
}
147+
148+
.chapter-nav-btn {
149+
max-width: 100%;
150+
width: 100%;
151+
}
152+
153+
.chapter-nav-btn.next {
154+
align-items: flex-start;
155+
text-align: left;
156+
}
157+
}
158+
</style>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
import { ArrowRight } from '@lucide/astro';
3+
4+
interface Props {
5+
index: number;
6+
name: string;
7+
path: string;
8+
docstring: string;
9+
annotation?: {
10+
version: string;
11+
feature: string;
12+
} | null;
13+
activeLang: string;
14+
viewCodeText?: string;
15+
}
16+
17+
const {
18+
index,
19+
name,
20+
path,
21+
docstring,
22+
annotation,
23+
activeLang = 'en',
24+
viewCodeText = 'View Code',
25+
} = Astro.props;
26+
27+
const sequence = String(index + 1).padStart(2, '0');
28+
const firstParagraph = docstring.split(/\n\s*\n/)[0].trim();
29+
---
30+
31+
<a href={`/ultimate-python/${activeLang}/${path}/`} class="lesson-card">
32+
<div class="card-header">
33+
<span class="card-index">{sequence}</span>
34+
<h3 class="card-title">{name}</h3>
35+
{annotation && (
36+
<span class="version-badge" title={annotation.feature}>
37+
{annotation.version}
38+
</span>
39+
)}
40+
</div>
41+
42+
<p class="card-docstring">{firstParagraph}</p>
43+
44+
<div class="card-footer">
45+
<span class="card-action">
46+
{viewCodeText}
47+
<ArrowRight size={14} class="action-arrow" />
48+
</span>
49+
</div>
50+
</a>
51+
52+
<style>
53+
.lesson-card {
54+
display: flex;
55+
flex-direction: column;
56+
padding: 1.5rem;
57+
border-radius: 8px;
58+
border: 1px solid var(--border-color);
59+
background-color: var(--bg-secondary);
60+
color: var(--text-primary);
61+
text-decoration: none;
62+
transition:
63+
transform var(--transition-speed),
64+
border-color var(--transition-speed),
65+
box-shadow var(--transition-speed);
66+
height: 100%;
67+
}
68+
69+
.lesson-card:hover {
70+
transform: translateY(-2px);
71+
border-color: var(--forest-green);
72+
box-shadow: 0 6px 16px rgba(27, 67, 50, 0.08);
73+
}
74+
75+
.card-header {
76+
display: flex;
77+
align-items: baseline;
78+
gap: 0.65rem;
79+
margin-bottom: 0.75rem;
80+
flex-wrap: wrap;
81+
}
82+
83+
.card-index {
84+
font-family: var(--font-mono);
85+
font-size: 0.85rem;
86+
font-weight: 600;
87+
color: var(--forest-green);
88+
background-color: var(--forest-green-soft);
89+
padding: 0.15rem 0.4rem;
90+
border-radius: 4px;
91+
line-height: 1;
92+
}
93+
94+
.card-title {
95+
font-size: 1.15rem;
96+
font-family: var(--font-serif);
97+
font-weight: 700;
98+
color: var(--text-primary);
99+
margin: 0;
100+
line-height: 1.3;
101+
flex: 1;
102+
}
103+
104+
.lesson-card:hover .card-title {
105+
color: var(--forest-green);
106+
}
107+
108+
.version-badge {
109+
font-size: 0.72rem;
110+
font-family: var(--font-mono);
111+
padding: 0.2rem 0.5rem;
112+
background-color: var(--bg-tertiary);
113+
border: 1px solid var(--border-color);
114+
border-radius: 4px;
115+
color: var(--text-secondary);
116+
font-weight: 500;
117+
}
118+
119+
.card-docstring {
120+
font-size: 0.9rem;
121+
color: var(--text-secondary);
122+
line-height: 1.55;
123+
margin-bottom: 1.25rem;
124+
flex: 1;
125+
display: -webkit-box;
126+
-webkit-line-clamp: 3;
127+
line-clamp: 3;
128+
-webkit-box-orient: vertical;
129+
overflow: hidden;
130+
}
131+
132+
.card-footer {
133+
display: flex;
134+
align-items: center;
135+
justify-content: flex-end;
136+
margin-top: auto;
137+
padding-top: 0.75rem;
138+
border-top: 1px solid var(--border-color);
139+
}
140+
141+
.card-action {
142+
display: inline-flex;
143+
align-items: center;
144+
gap: 0.35rem;
145+
font-size: 0.85rem;
146+
font-weight: 600;
147+
color: var(--forest-green);
148+
font-family: var(--font-sans);
149+
}
150+
151+
:global(.action-arrow) {
152+
transition: transform var(--transition-speed);
153+
}
154+
155+
.lesson-card:hover :global(.action-arrow) {
156+
transform: translateX(4px);
157+
}
158+
</style>

‎ui/src/components/sidebar/Sidebar.astro‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const categoriesMap = lessons.reduce(
129129

130130
return (
131131
<SidebarCategory
132+
id={catKey}
132133
name={cat.name}
133134
lessons={cat.lessons}
134135
activePath={activePath}
@@ -313,6 +314,7 @@ const categoriesMap = lessons.reduce(
313314
const stored = localStorage.getItem('sidebar-collapsed');
314315
const isMobile = window.innerWidth < 900;
315316
const isCollapsed = isMobile || stored === 'true';
317+
document.documentElement.classList.remove('sidebar-mobile-open');
316318
if (isCollapsed) {
317319
document.documentElement.classList.add('sidebar-collapsed');
318320
} else {
@@ -326,6 +328,16 @@ const categoriesMap = lessons.reduce(
326328
localStorage.setItem('sidebar-collapsed', 'true');
327329
};
328330

331+
// Close mobile drawer when navigating via sidebar links
332+
const navLinks = document.querySelectorAll('.sidebar-nav a, .sidebar-logo');
333+
navLinks.forEach((link) => {
334+
link.addEventListener('click', () => {
335+
if (window.innerWidth < 900) {
336+
closeSidebar();
337+
}
338+
});
339+
});
340+
329341
toggleBtn.addEventListener('click', () => {
330342
document.documentElement.classList.add('sidebar-transitioning');
331343
const collapsed =

0 commit comments

Comments
 (0)