Compare commits

..

No commits in common. "0528df2d37220dfc39272a5b1dfd44d6dfbc166d" and "d3acfcf825ff201e7dfe0bd57851ed9d990e4af0" have entirely different histories.

5 changed files with 12 additions and 77 deletions

View File

@ -16,9 +16,8 @@
let container: HTMLDivElement; let container: HTMLDivElement;
let view: any; let view: any;
let ready = $state(false); let ready = $state(false);
let saving = $state(false);
let lastThemeDark: boolean | undefined; let lastThemeDark: boolean | undefined;
let lastStorageKey = $state<string | undefined>(undefined); let lastStorageKey = storageKey;
// Store module references after dynamic import // Store module references after dynamic import
let CM: any; let CM: any;
@ -27,11 +26,9 @@
function saveToStorage(value: string) { function saveToStorage(value: string) {
if (!storageKey) return; if (!storageKey) return;
saving = true;
clearTimeout(saveTimeout); clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => { saveTimeout = setTimeout(() => {
sessionStorage.setItem(storageKey, value); sessionStorage.setItem(storageKey, value);
saving = false;
}, 1000); }, 1000);
} }
@ -289,66 +286,22 @@
return view?.state.doc.toString() ?? code; return view?.state.doc.toString() ?? code;
} }
</script> </script>
<div class="editor-wrapper" class:no-paste={noPaste} bind:this={container}> <div class="editor-wrapper" class:no-paste={noPaste} bind:this={container}>
{#if !ready} {#if !ready}
<div class="editor-loading">Memuat editor...</div> <div class="editor-loading">Memuat editor...</div>
{/if} {/if}
{#if storageKey}
<div class="storage-indicator" title={saving ? "Menyimpan draf..." : "Draf tersimpan di browser"}>
<span class="indicator-icon" class:saving>
{saving ? '●' : '☁'}
</span>
<span class="indicator-text">Auto-save</span>
</div>
{/if}
</div> </div>
<style> <style>
.editor-wrapper { .editor-wrapper {
position: relative;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius); border-radius: var(--radius);
overflow: hidden; overflow: hidden;
font-size: 0.9rem; font-size: 0.9rem;
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
.storage-indicator {
position: absolute;
bottom: 8px;
right: 12px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 12px;
font-size: 0.7rem;
color: var(--color-text-muted);
pointer-events: none;
opacity: 0.8;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.indicator-icon {
line-height: 1;
font-size: 0.8rem;
color: var(--color-success);
}
.indicator-icon.saving {
color: var(--color-primary);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
.indicator-text {
font-weight: 500;
}
.editor-wrapper :global(.cm-editor) { .editor-wrapper :global(.cm-editor) {
...
min-height: 200px; min-height: 200px;
max-height: 60vh; max-height: 60vh;
} }

View File

@ -37,11 +37,9 @@ export const auth = {
authIsTeacher.set(res.is_teacher ?? false); authIsTeacher.set(res.is_teacher ?? false);
} else { } else {
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
} }
} catch { } catch {
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
} }
}, },
@ -53,7 +51,6 @@ export const auth = {
authLoggedIn.set(true); authLoggedIn.set(true);
authIsTeacher.set(res.is_teacher ?? false); authIsTeacher.set(res.is_teacher ?? false);
localStorage.setItem(STORAGE_KEY, inputToken); localStorage.setItem(STORAGE_KEY, inputToken);
sessionStorage.clear();
} }
return res; return res;
}, },
@ -65,6 +62,5 @@ export const auth = {
authLoggedIn.set(false); authLoggedIn.set(false);
authIsTeacher.set(false); authIsTeacher.set(false);
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
sessionStorage.clear();
} }
}; };

View File

@ -5,7 +5,7 @@
import OutputPanel from '$components/OutputPanel.svelte'; import OutputPanel from '$components/OutputPanel.svelte';
import CelebrationOverlay from '$components/CelebrationOverlay.svelte'; import CelebrationOverlay from '$components/CelebrationOverlay.svelte';
import { compileCode, trackProgress } from '$services/api'; import { compileCode, trackProgress } from '$services/api';
import { auth, authLoggedIn } from '$stores/auth'; import { auth } from '$stores/auth';
import { lessonContext } from '$stores/lessonContext'; import { lessonContext } from '$stores/lessonContext';
import { noSelect } from '$actions/noSelect'; import { noSelect } from '$actions/noSelect';
import { createFloatingPanel } from '$actions/floatingPanel.svelte'; import { createFloatingPanel } from '$actions/floatingPanel.svelte';
@ -20,7 +20,6 @@
let lesson = $derived(pageData.lesson); let lesson = $derived(pageData.lesson);
let data = $state<LessonContent | null>(null); let data = $state<LessonContent | null>(null);
let lessonCompleted = $state(false);
let currentCode = $state(''); let currentCode = $state('');
let compileOutput = $state(''); let compileOutput = $state('');
let compileError = $state(''); let compileError = $state('');
@ -91,7 +90,6 @@
$effect(() => { $effect(() => {
if (lesson) { if (lesson) {
data = lesson; data = lesson;
lessonCompleted = lesson.lesson_completed;
currentCode = lesson.initial_code ?? ''; currentCode = lesson.initial_code ?? '';
compileOutput = ''; compileOutput = '';
compileError = ''; compileError = '';
@ -158,7 +156,7 @@
if (auth.isLoggedIn) { if (auth.isLoggedIn) {
const lessonName = slug.replace('.md', ''); const lessonName = slug.replace('.md', '');
await trackProgress(auth.token, lessonName); await trackProgress(auth.token, lessonName);
lessonCompleted = true; data.lesson_completed = true;
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx); lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
} }
// Auto-show solution after celebration // Auto-show solution after celebration
@ -344,7 +342,7 @@
{compiling ? 'Compiling...' : '\u25B6 Run'} {compiling ? 'Compiling...' : '\u25B6 Run'}
</button> </button>
<button type="button" class="btn btn-secondary" onclick={handleReset}>Reset</button> <button type="button" class="btn btn-secondary" onclick={handleReset}>Reset</button>
{#if data.solution_code && $authLoggedIn && lessonCompleted} {#if data.solution_code && auth.isLoggedIn && data.lesson_completed}
<button type="button" class="btn btn-secondary" onclick={handleShowSolution}> <button type="button" class="btn btn-secondary" onclick={handleShowSolution}>
{showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'} {showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'}
</button> </button>
@ -358,7 +356,7 @@
code={currentCode} code={currentCode}
language={data.language} language={data.language}
noPaste={true} noPaste={true}
storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}` : undefined} storageKey={showSolution ? undefined : `elemes_draft_${slug}`}
onchange={(val) => { if (!showSolution) currentCode = val; }} onchange={(val) => { if (!showSolution) currentCode = val; }}
/> />
</div> </div>

View File

@ -5,9 +5,8 @@ bind = "0.0.0.0:5000"
backlog = 2048 backlog = 2048
# Worker processes # Worker processes
workers = 2 workers = 4
worker_class = "gthread" worker_class = "sync"
threads = 4
worker_connections = 1000 worker_connections = 1000
timeout = 120 timeout = 120
keepalive = 5 keepalive = 5

View File

@ -4,14 +4,12 @@ Lesson loading, ordering, and markdown rendering.
import os import os
import re import re
from functools import lru_cache
import markdown as md import markdown as md
from config import CONTENT_DIR from config import CONTENT_DIR
@lru_cache(maxsize=1)
def _read_home_md(): def _read_home_md():
"""Read home.md and return its content, or empty string if missing.""" """Read home.md and return its content, or empty string if missing."""
path = os.path.join(CONTENT_DIR, "home.md") path = os.path.join(CONTENT_DIR, "home.md")
@ -34,7 +32,6 @@ def _parse_lesson_links(home_content):
# Lesson listing # Lesson listing
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@lru_cache(maxsize=32)
def get_lessons(): def get_lessons():
"""Get lessons from the Available_Lessons section in home.md.""" """Get lessons from the Available_Lessons section in home.md."""
lessons = [] lessons = []
@ -74,7 +71,6 @@ def get_lessons():
return lessons return lessons
@lru_cache(maxsize=32)
def get_lesson_names(): def get_lesson_names():
"""Get lesson names (without .md extension) from Available_Lessons.""" """Get lesson names (without .md extension) from Available_Lessons."""
home_content = _read_home_md() home_content = _read_home_md()
@ -89,7 +85,6 @@ def get_lesson_names():
return names return names
@lru_cache(maxsize=32)
def get_lessons_with_learning_objectives(): def get_lessons_with_learning_objectives():
"""Get lessons with learning objectives extracted from LESSON_INFO sections.""" """Get lessons with learning objectives extracted from LESSON_INFO sections."""
lessons = [] lessons = []
@ -176,18 +171,14 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
seen = {l['filename'] for l in ordered} seen = {l['filename'] for l in ordered}
for lesson in all_lessons: for lesson in all_lessons:
if lesson['filename'] not in seen: if lesson['filename'] not in seen:
copy = lesson.copy() _add_completion(lesson, progress)
_add_completion(copy, progress) ordered.append(lesson)
ordered.append(copy)
return ordered return ordered
ordered_fallback = []
for lesson in all_lessons: for lesson in all_lessons:
copy = lesson.copy() _add_completion(lesson, progress)
_add_completion(copy, progress) return all_lessons
ordered_fallback.append(copy)
return ordered_fallback
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -212,7 +203,6 @@ def _extract_section(content, start_marker, end_marker):
return extracted, remaining return extracted, remaining
@lru_cache(maxsize=32)
def render_markdown_content(file_path): def render_markdown_content(file_path):
"""Parse a lesson markdown file and return structured HTML parts. """Parse a lesson markdown file and return structured HTML parts.
@ -260,7 +250,6 @@ def render_markdown_content(file_path):
return lesson_html, exercise_html, expected_output, lesson_info_html, initial_code, solution_code, key_text return lesson_html, exercise_html, expected_output, lesson_info_html, initial_code, solution_code, key_text
@lru_cache(maxsize=1)
def render_home_content(): def render_home_content():
"""Render the home.md intro section (before Available_Lessons) as HTML.""" """Render the home.md intro section (before Available_Lessons) as HTML."""
home_content = _read_home_md() home_content = _read_home_md()