Skip to content

Commit 734d93a

Browse files
committed
Make the UI mobile friendly
1 parent 7e3d58a commit 734d93a

7 files changed

Lines changed: 258 additions & 138 deletions

File tree

ui/astro.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export default defineConfig({
44
site: 'https://sambyte.net',
55
base: '/ultimate-python',
66
output: 'static',
7+
prefetch: {
8+
defaultStrategy: 'hover'
9+
}
710
});

ui/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"astro": "astro"
1111
},
1212
"dependencies": {
13+
"@lucide/astro": "^1.23.0",
1314
"astro": "^7.0.6"
1415
},
1516
"devDependencies": {

ui/parse_lessons.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,6 @@ def parse_file(filepath, relative_path):
2222
except Exception:
2323
docstring = ""
2424

25-
# Strip leading docstring from code display
26-
code_clean = content
27-
stripped = content.strip()
28-
if docstring:
29-
if stripped.startswith('"""'):
30-
end_idx = stripped.find('"""', 3)
31-
if end_idx != -1:
32-
code_clean = stripped[end_idx + 3 :].strip()
33-
elif stripped.startswith("'''"):
34-
end_idx = stripped.find("'''", 3)
35-
if end_idx != -1:
36-
code_clean = stripped[end_idx + 3 :].strip()
37-
3825
category = relative_path.split(os.sep)[0]
3926
filename = os.path.basename(filepath)
4027
name_without_ext = os.path.splitext(filename)[0]
@@ -47,8 +34,7 @@ def parse_file(filepath, relative_path):
4734
"category": category,
4835
"category_name": CATEGORY_MAP.get(category, category.capitalize()),
4936
"docstring": docstring.strip(),
50-
"code": code_clean,
51-
"raw_code": content,
37+
"code": content,
5238
}
5339

5440

ui/src/components/Sidebar.astro

Lines changed: 126 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import lessons from '../data/lessons.json';
33
import { languages, useTranslations } from '../i18n/ui';
4+
import { ChevronLeft, ChevronRight } from '@lucide/astro';
45
56
interface Props {
67
activePath?: string;
@@ -28,22 +29,21 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
2829

2930
<aside class="sidebar">
3031
<button id="sidebar-toggle" class="sidebar-toggle-btn" aria-label="Toggle Sidebar">
31-
<span>←</span>
32+
<span class="arrow-left"><ChevronLeft size={16} /></span>
33+
<span class="arrow-right"><ChevronRight size={16} /></span>
3234
</button>
3335

3436
<script is:inline>
3537
// Run this immediately to prevent layout shift/flash
3638
(function() {
37-
const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
39+
const stored = localStorage.getItem('sidebar-collapsed');
40+
const isMobile = window.innerWidth < 900;
41+
const isCollapsed = stored === 'true' || (stored === null && isMobile);
3842
if (isCollapsed) {
3943
const container = document.querySelector('.app-container');
4044
if (container) {
4145
container.classList.add('sidebar-collapsed');
4246
}
43-
const btn = document.getElementById('sidebar-toggle');
44-
if (btn) {
45-
btn.innerHTML = '<span>→</span>';
46-
}
4747
}
4848
})();
4949
</script>
@@ -110,6 +110,7 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
110110
})}
111111
</nav>
112112
</aside>
113+
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
113114

114115
<style>
115116
.sidebar {
@@ -127,7 +128,7 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
127128
.sidebar-toggle-btn {
128129
position: absolute;
129130
right: -15px;
130-
top: 25px;
131+
top: 15px;
131132
width: 30px;
132133
height: 30px;
133134
border-radius: 50%;
@@ -146,6 +147,24 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
146147
font-weight: bold;
147148
}
148149

150+
.arrow-left, .arrow-right {
151+
display: flex;
152+
align-items: center;
153+
justify-content: center;
154+
}
155+
156+
.arrow-right {
157+
display: none;
158+
}
159+
160+
:global(.app-container.sidebar-collapsed) .arrow-left {
161+
display: none;
162+
}
163+
164+
:global(.app-container.sidebar-collapsed) .arrow-right {
165+
display: flex;
166+
}
167+
149168
.sidebar-toggle-btn:hover {
150169
background-color: var(--bg-tertiary);
151170
transform: scale(1.05);
@@ -327,6 +346,35 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
327346
.lesson-link.active .lesson-indicator {
328347
background-color: var(--forest-green);
329348
}
349+
350+
.sidebar-backdrop {
351+
display: none;
352+
}
353+
354+
@media (max-width: 900px) {
355+
.sidebar {
356+
position: fixed;
357+
left: 0;
358+
top: 0;
359+
bottom: 0;
360+
z-index: 1000;
361+
box-shadow: 5px 0 15px rgba(0, 0, 0, 0.1);
362+
}
363+
364+
365+
366+
:global(.app-container:not(.sidebar-collapsed)) .sidebar-backdrop {
367+
display: block;
368+
position: fixed;
369+
top: 0;
370+
left: 0;
371+
right: 0;
372+
bottom: 0;
373+
background-color: rgba(0, 0, 0, 0.4);
374+
backdrop-filter: blur(2px);
375+
z-index: 999;
376+
}
377+
}
330378
</style>
331379

332380
<script>
@@ -336,46 +384,52 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
336384
if (!searchInput || searchInput.dataset.searchInitialized) return;
337385
searchInput.dataset.searchInitialized = 'true';
338386

387+
let searchTimeout: number;
339388
searchInput.addEventListener('input', (e) => {
340-
const query = (e.target as HTMLInputElement).value.toLowerCase().trim();
341-
const items = document.querySelectorAll('.lesson-item');
342-
const groups = document.querySelectorAll('.category-group');
343-
344-
if (!query) {
345-
// Reset visibility
346-
items.forEach(el => el.classList.remove('hidden-search'));
347-
groups.forEach(el => el.classList.remove('hidden-search'));
348-
return;
349-
}
350-
351-
// Track which categories have visible matches
352-
const categoryMatches = new Map();
389+
cancelAnimationFrame(searchTimeout);
390+
const target = e.target as HTMLInputElement;
353391

354-
items.forEach(item => {
355-
const name = item.getAttribute('data-lesson-name') || '';
356-
const desc = item.getAttribute('data-lesson-desc') || '';
357-
const id = item.getAttribute('data-lesson-id') || '';
392+
searchTimeout = requestAnimationFrame(() => {
393+
const query = target.value.toLowerCase().trim();
394+
const items = document.querySelectorAll('.lesson-item');
395+
const groups = document.querySelectorAll('.category-group');
358396

359-
const isMatch = name.includes(query) || desc.includes(query) || id.includes(query);
397+
if (!query) {
398+
// Reset visibility
399+
items.forEach(el => el.classList.remove('hidden-search'));
400+
groups.forEach(el => el.classList.remove('hidden-search'));
401+
return;
402+
}
360403

361-
const parentGroup = item.closest('.category-group');
362-
if (parentGroup) {
363-
if (isMatch) {
364-
item.classList.remove('hidden-search');
365-
categoryMatches.set(parentGroup, true);
404+
// Track which categories have visible matches
405+
const categoryMatches = new Map();
406+
407+
items.forEach(item => {
408+
const name = item.getAttribute('data-lesson-name') || '';
409+
const desc = item.getAttribute('data-lesson-desc') || '';
410+
const id = item.getAttribute('data-lesson-id') || '';
411+
412+
const isMatch = name.includes(query) || desc.includes(query) || id.includes(query);
413+
414+
const parentGroup = item.closest('.category-group');
415+
if (parentGroup) {
416+
if (isMatch) {
417+
item.classList.remove('hidden-search');
418+
categoryMatches.set(parentGroup, true);
419+
} else {
420+
item.classList.add('hidden-search');
421+
}
422+
}
423+
});
424+
425+
groups.forEach(group => {
426+
if (categoryMatches.has(group)) {
427+
group.classList.remove('hidden-search');
428+
(group as HTMLDetailsElement).open = true;
366429
} else {
367-
item.classList.add('hidden-search');
430+
group.classList.add('hidden-search');
368431
}
369-
}
370-
});
371-
372-
groups.forEach(group => {
373-
if (categoryMatches.has(group)) {
374-
group.classList.remove('hidden-search');
375-
(group as HTMLDetailsElement).open = true;
376-
} else {
377-
group.classList.add('hidden-search');
378-
}
432+
});
379433
});
380434
});
381435
}
@@ -405,27 +459,51 @@ const categoryOrder = ["syntax", "data_structures", "classes", "advanced"];
405459
function setupSidebarToggle() {
406460
const toggleBtn = document.getElementById('sidebar-toggle');
407461
const container = document.querySelector('.app-container');
462+
const backdrop = document.getElementById('sidebar-backdrop');
408463
if (!toggleBtn || !container || toggleBtn.dataset.toggleInitialized) return;
409464
toggleBtn.dataset.toggleInitialized = 'true';
410465

411466
// Load initial state
412-
const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
467+
const stored = localStorage.getItem('sidebar-collapsed');
468+
const isMobile = window.innerWidth < 900;
469+
const isCollapsed = stored === 'true' || (stored === null && isMobile);
413470
if (isCollapsed) {
414471
container.classList.add('sidebar-collapsed');
415-
toggleBtn.innerHTML = '<span>→</span>';
416472
} else {
417473
container.classList.remove('sidebar-collapsed');
418-
toggleBtn.innerHTML = '<span>←</span>';
419474
}
420475

476+
const closeSidebar = () => {
477+
container.classList.add('sidebar-collapsed');
478+
localStorage.setItem('sidebar-collapsed', 'true');
479+
};
480+
421481
toggleBtn.addEventListener('click', () => {
422482
const collapsed = container.classList.toggle('sidebar-collapsed');
423483
localStorage.setItem('sidebar-collapsed', String(collapsed));
424-
if (collapsed) {
425-
toggleBtn.innerHTML = '<span>→</span>';
426-
} else {
427-
toggleBtn.innerHTML = '<span>←</span>';
484+
});
485+
486+
if (backdrop) {
487+
backdrop.addEventListener('click', closeSidebar);
488+
}
489+
490+
// Auto-collapse/expand on window resize transition
491+
let lastWidth = window.innerWidth;
492+
window.addEventListener('resize', () => {
493+
const currentWidth = window.innerWidth;
494+
const wasDesktop = lastWidth >= 900;
495+
const isMobile = currentWidth < 900;
496+
497+
if (wasDesktop && isMobile) {
498+
container.classList.add('sidebar-collapsed');
499+
} else if (!wasDesktop && !isMobile) {
500+
const stored = localStorage.getItem('sidebar-collapsed');
501+
const isCollapsed = stored === 'true';
502+
if (!isCollapsed) {
503+
container.classList.remove('sidebar-collapsed');
504+
}
428505
}
506+
lastWidth = currentWidth;
429507
});
430508
}
431509

0 commit comments

Comments
 (0)