feat: Add a visual auto-save indicator to the code editor, clear session storage on authentication changes, and integrate auto-save functionality with lesson progress and solution viewing.
parent
d3acfcf825
commit
9b745f52f4
|
|
@ -16,8 +16,9 @@
|
|||
let container: HTMLDivElement;
|
||||
let view: any;
|
||||
let ready = $state(false);
|
||||
let saving = $state(false);
|
||||
let lastThemeDark: boolean | undefined;
|
||||
let lastStorageKey = storageKey;
|
||||
let lastStorageKey = $state<string | undefined>(undefined);
|
||||
|
||||
// Store module references after dynamic import
|
||||
let CM: any;
|
||||
|
|
@ -26,9 +27,11 @@
|
|||
|
||||
function saveToStorage(value: string) {
|
||||
if (!storageKey) return;
|
||||
saving = true;
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(() => {
|
||||
sessionStorage.setItem(storageKey, value);
|
||||
saving = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
|
@ -286,22 +289,66 @@
|
|||
return view?.state.doc.toString() ?? code;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper" class:no-paste={noPaste} bind:this={container}>
|
||||
{#if !ready}
|
||||
<div class="editor-loading">Memuat editor...</div>
|
||||
{/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>
|
||||
|
||||
<style>
|
||||
.editor-wrapper {
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
-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) {
|
||||
...
|
||||
min-height: 200px;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,11 @@ export const auth = {
|
|||
authIsTeacher.set(res.is_teacher ?? false);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
sessionStorage.clear();
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
sessionStorage.clear();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -51,6 +53,7 @@ export const auth = {
|
|||
authLoggedIn.set(true);
|
||||
authIsTeacher.set(res.is_teacher ?? false);
|
||||
localStorage.setItem(STORAGE_KEY, inputToken);
|
||||
sessionStorage.clear();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
|
@ -62,5 +65,6 @@ export const auth = {
|
|||
authLoggedIn.set(false);
|
||||
authIsTeacher.set(false);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
sessionStorage.clear();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import OutputPanel from '$components/OutputPanel.svelte';
|
||||
import CelebrationOverlay from '$components/CelebrationOverlay.svelte';
|
||||
import { compileCode, trackProgress } from '$services/api';
|
||||
import { auth } from '$stores/auth';
|
||||
import { auth, authLoggedIn } from '$stores/auth';
|
||||
import { lessonContext } from '$stores/lessonContext';
|
||||
import { noSelect } from '$actions/noSelect';
|
||||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
let lesson = $derived(pageData.lesson);
|
||||
|
||||
let data = $state<LessonContent | null>(null);
|
||||
let lessonCompleted = $state(false);
|
||||
let currentCode = $state('');
|
||||
let compileOutput = $state('');
|
||||
let compileError = $state('');
|
||||
|
|
@ -90,6 +91,7 @@
|
|||
$effect(() => {
|
||||
if (lesson) {
|
||||
data = lesson;
|
||||
lessonCompleted = lesson.lesson_completed;
|
||||
currentCode = lesson.initial_code ?? '';
|
||||
compileOutput = '';
|
||||
compileError = '';
|
||||
|
|
@ -156,7 +158,7 @@
|
|||
if (auth.isLoggedIn) {
|
||||
const lessonName = slug.replace('.md', '');
|
||||
await trackProgress(auth.token, lessonName);
|
||||
data.lesson_completed = true;
|
||||
lessonCompleted = true;
|
||||
lessonContext.update(ctx => ctx ? { ...ctx, completed: true } : ctx);
|
||||
}
|
||||
// Auto-show solution after celebration
|
||||
|
|
@ -342,7 +344,7 @@
|
|||
{compiling ? 'Compiling...' : '\u25B6 Run'}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick={handleReset}>Reset</button>
|
||||
{#if data.solution_code && auth.isLoggedIn && data.lesson_completed}
|
||||
{#if data.solution_code && $authLoggedIn && lessonCompleted}
|
||||
<button type="button" class="btn btn-secondary" onclick={handleShowSolution}>
|
||||
{showSolution ? 'Sembunyikan Solusi' : 'Lihat Solusi'}
|
||||
</button>
|
||||
|
|
@ -356,7 +358,7 @@
|
|||
code={currentCode}
|
||||
language={data.language}
|
||||
noPaste={true}
|
||||
storageKey={showSolution ? undefined : `elemes_draft_${slug}`}
|
||||
storageKey={($authLoggedIn && !showSolution) ? `elemes_draft_${slug}` : undefined}
|
||||
onchange={(val) => { if (!showSolution) currentCode = val; }}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue