diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index f17acb1..27040e8 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -42,6 +42,8 @@ {#if showDropdown} + + +
+ +
diff --git a/frontend/src/routes/lesson/[slug]/CodeTab.svelte b/frontend/src/routes/lesson/[slug]/CodeTab.svelte new file mode 100644 index 0000000..d7d73b9 --- /dev/null +++ b/frontend/src/routes/lesson/[slug]/CodeTab.svelte @@ -0,0 +1,65 @@ + + +
+ + + {#if data.solution_code && authLoggedIn && lessonCompleted} + + {/if} + {currentLanguage === 'python' ? 'Python' : 'C'} +
+ +
+ {#key currentLanguage} + { if (!showSolution) currentCode = val; }} + /> + {/key} +
+ +{#if data.expected_output} +
+ Expected Output +
{data.expected_output}
+
+{/if} diff --git a/frontend/src/routes/lesson/[slug]/VelxioTab.svelte b/frontend/src/routes/lesson/[slug]/VelxioTab.svelte new file mode 100644 index 0000000..344ec84 --- /dev/null +++ b/frontend/src/routes/lesson/[slug]/VelxioTab.svelte @@ -0,0 +1,38 @@ + + +{#if velxioError} +
+ Simulator Arduino sedang tidak tersedia. + Hubungi guru jika masalah berlanjut. +
+{:else} + {#if authLoggedIn} +
+ + {velxioSaving ? '●' : '☁'} + + Auto-save +
+ {/if} + + +{/if} diff --git a/frontend/src/routes/lesson/[slug]/lesson.css b/frontend/src/routes/lesson/[slug]/lesson.css new file mode 100644 index 0000000..3b813b6 --- /dev/null +++ b/frontend/src/routes/lesson/[slug]/lesson.css @@ -0,0 +1,298 @@ +/* ── Shared rich-text styles (.prose + .tab-content) ──── */ +.tab-content { + font-size: 0.85rem; + padding: 0.75rem 0.5rem; + line-height: 1.65; +} +.tab-heading { + color: var(--color-primary); + font-size: 1.1rem; + margin-top: 0; +} +.prose pre, +.tab-content pre { + background: var(--color-bg-secondary); + padding: 0.75rem; + border-radius: var(--radius); + overflow-x: auto; +} +.prose code, +.tab-content code { + font-family: var(--font-mono); + font-size: 0.85rem; +} +.prose p, +.tab-content p { + margin-bottom: 0.75rem; +} +.prose h2, .prose h3, +.tab-content h2, .tab-content h3 { + margin-top: 1.25rem; + margin-bottom: 0.5rem; +} +.tab-content ul, +.tab-content ol { + margin-bottom: 0.75rem; + padding-left: 1.5rem; +} +.tab-content li { + margin-bottom: 0.25rem; +} + +/* ── Two-column layout ─────────────────────────────────── */ +.lesson-layout { + display: grid; + grid-template-columns: 3fr 2fr; + gap: 1.5rem; + align-items: start; +} + +.lesson-content { + overflow-y: auto; + max-height: 90vh; + padding-right: 0.5rem; + -webkit-user-select: none; + user-select: none; + -webkit-touch-callout: none; +} + +/* ── Editor area (docked mode) ──────────────────────────── */ +.editor-area { + position: sticky; + top: 3.5rem; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + overflow: hidden; + max-height: 85vh; +} +.editor-area .editor-body { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0.5rem; + min-height: 0; +} + +/* ── Single-column layout ──────────────────────────────── */ +.lesson-layout.single-col { + grid-template-columns: 1fr; +} +.lesson-content.full-width { + max-height: none; + padding-right: 0; + padding-bottom: 60px; +} + +/* ── Toolbar ───────────────────────────────────────────── */ +.toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} +.btn-secondary { + background: var(--color-bg-secondary); + color: var(--color-text); + border: 1px solid var(--color-border); +} +.btn-secondary:hover { + background: var(--color-border); +} +.btn-run-all { + padding: 0.3rem 0.6rem; + font-size: 0.8rem; +} +.lang-label { + margin-left: auto; + font-size: 0.75rem; + color: var(--color-text-muted); + font-weight: 600; + text-transform: uppercase; +} +.panel { + margin-bottom: 0.75rem; +} +.expected-output { + font-size: 0.8rem; + color: var(--color-text-muted); +} +.expected-output pre { + background: var(--color-bg-secondary); + padding: 0.5rem; + border-radius: var(--radius); + margin-top: 0.5rem; +} + +/* ── Floating restore button ────────────────────────────── */ +.float-restore-btn { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 9999; + background: var(--color-primary); + color: #fff; + border: none; + border-radius: var(--radius); + padding: 0.6rem 1rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + transition: background 0.15s, transform 0.1s; +} +.float-restore-btn:hover { + background: var(--color-primary-dark); +} +.float-restore-btn:active { + transform: scale(0.95); +} + +/* ── Desktop floating mode ─────────────────────────────── */ +.editor-area.floating { + position: fixed; + bottom: 1rem; + right: 1rem; + top: auto; + width: 45vw; + height: 70vh; + min-width: 320px; + max-width: 100vw; + max-height: 100vh; + z-index: 9999; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + overflow: hidden; +} +.editor-area.floating-hidden { + display: none !important; +} + +/* ── Mobile bottom sheet ──────────────────────────────── */ +.editor-area.mobile-sheet { + position: fixed; + top: auto; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; + background: var(--color-bg); + border-top: 2px solid var(--color-primary); + border-radius: 12px 12px 0 0; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + overflow: hidden; + transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.2s ease; +} +.editor-area.mobile-hidden { + height: 52px; +} +.editor-area.mobile-half { + height: 60vh; +} +.editor-area.mobile-full { + height: calc(100vh - 3rem); + border-radius: 0; +} +.mobile-sheet .editor-body { + overscroll-behavior: contain; +} +/* ── Mobile full: expand content to fill ────────────── */ +.editor-area.mobile-full .editor-body, +.editor-area.mobile-full .tab-panel:not(.tab-hidden), +.editor-area.mobile-full .panel, +.editor-area.mobile-full .circuit-container, +.editor-area.mobile-full .editor-wrapper { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +.editor-area.mobile-full .circuit-wrapper { + flex: 1; + height: auto; +} +.editor-area.mobile-full .cm-editor { + flex: 1; + max-height: none; + min-height: 0; +} +.editor-area.mobile-full .cm-scroller { + flex: 1; +} + +/* ── Tab panels ────────────────────────────────────────── */ +.tab-panel { + overflow-y: auto; +} +.tab-hidden { + display: none; +} + +/* ── Velxio (Arduino simulator) ─────────────────────── */ +.storage-indicator-inline { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + color: var(--color-text-muted); + background: var(--color-bg-secondary); + padding: 3px 10px; + border-radius: 12px; + border: 1px solid var(--color-border); + position: absolute; + bottom: 1rem; + right: 1.5rem; + z-index: 10; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); + pointer-events: none; + opacity: 0.8; +} +.storage-indicator-inline .indicator-icon { + line-height: 1; + font-size: 0.8rem; + color: var(--color-success); +} +.storage-indicator-inline .indicator-icon.saving { + color: var(--color-primary); + animation: pulse 1s infinite; +} +.storage-indicator-inline .indicator-text { + font-weight: 500; +} + +.velxio-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + position: relative; +} +.velxio-panel.tab-hidden { + display: none; +} +.velxio-iframe { + flex: 1; + width: 100%; + min-height: 400px; + border: none; + border-radius: var(--radius); +} +.velxio-fallback { + padding: 2rem; + text-align: center; + color: var(--color-text-muted); + background: var(--color-bg-secondary); + border-radius: var(--radius); + margin: 0.5rem 0; +} \ No newline at end of file diff --git a/services/lesson_service.py b/services/lesson_service.py index a8db89b..021292e 100644 --- a/services/lesson_service.py +++ b/services/lesson_service.py @@ -24,11 +24,18 @@ def _read_home_md(): def _parse_lesson_links(home_content): """Extract (link_text, filename) pairs from the Available_Lessons section.""" - parts = home_content.split('---Available_Lessons---') + parts = re.split(r'-{3,}Available_Lessons-{3,}', home_content) if len(parts) <= 1: return [] - lesson_list_content = parts[1] - return re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', lesson_list_content) + lesson_list_content = parts[-1] + + links = re.findall(r'\[([^\]]+)\]\((?:/lesson/)?([^\)]+)\)', lesson_list_content) + + processed_links = [] + for title, slug in links: + filename = slug if slug.endswith('.md') else slug + '.md' + processed_links.append((title, filename)) + return processed_links # ---------------------------------------------------------------------------