181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
import { tick } from 'svelte';
|
|
import { getDraft, saveDraft, publishDraft, type DraftResponse } from './contentEditor';
|
|
import { renderMarkdownPreview } from './markdown';
|
|
|
|
export class ContentEditorManager {
|
|
activePath = $state<string | null>(null);
|
|
body = $state('');
|
|
draftId = $state<string | null>(null);
|
|
baseMtime = $state<number | null>(null);
|
|
dirty = $state(false);
|
|
loading = $state(false);
|
|
saving = $state(false);
|
|
publishing = $state(false);
|
|
|
|
// Preview state
|
|
previewHtml = $state('');
|
|
previewExerciseHtml = $state('');
|
|
previewQuizData = $state<unknown[]>([]);
|
|
previewSlides = $state<string[]>([]);
|
|
previewActiveTabs = $state<string[]>([]);
|
|
previewError = $state<string | null>(null);
|
|
previewLoading = $state(false);
|
|
|
|
// Mobile mode (replicated from LessonManager pattern)
|
|
mobileMode = $state<'hidden' | 'h30' | 'h50' | 'h70' | 'full'>('hidden');
|
|
isMobile = $state(false);
|
|
|
|
// Tab state — 'editor' | 'preview' | 'exercise' | 'quiz' | dynamic tabs from active_tabs ('c', 'python', 'circuit', 'velxio', 'flowchart')
|
|
activeTab = $state<string>('editor');
|
|
|
|
// Tree state
|
|
contentTree = $state<any[]>([]);
|
|
assetsTree = $state<any[]>([]);
|
|
treeRoot = $state<'content' | 'assets'>('content');
|
|
treeLoading = $state(false);
|
|
|
|
// Message feedback
|
|
lastMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
|
|
private previewTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
constructor() {
|
|
if (typeof window !== 'undefined') {
|
|
const mql = window.matchMedia('(max-width: 768px)');
|
|
this.isMobile = mql.matches;
|
|
const handler = (e: MediaQueryListEvent) => {
|
|
this.isMobile = e.matches;
|
|
};
|
|
mql.addEventListener('change', handler);
|
|
}
|
|
}
|
|
|
|
async loadDraft(targetPath: string) {
|
|
this.loading = true;
|
|
this.activePath = targetPath;
|
|
try {
|
|
const res = await getDraft(targetPath);
|
|
if (res.success) {
|
|
this.body = res.body ?? '';
|
|
this.draftId = res.draft_id;
|
|
this.baseMtime = res.base_mtime ?? null;
|
|
this.dirty = false;
|
|
// Trigger preview
|
|
this.schedulePreview();
|
|
} else {
|
|
this.showMessage('error', res.message || 'Gagal memuat file');
|
|
}
|
|
} catch {
|
|
this.showMessage('error', 'Gagal terhubung ke server');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
onBodyChange(newBody: string) {
|
|
this.body = newBody;
|
|
this.dirty = true;
|
|
this.schedulePreview();
|
|
}
|
|
|
|
private schedulePreview() {
|
|
if (this.previewTimeout) clearTimeout(this.previewTimeout);
|
|
this.previewTimeout = setTimeout(() => this.runPreview(), 500);
|
|
}
|
|
|
|
private runPreview() {
|
|
if (!this.body.trim()) {
|
|
this.previewHtml = '';
|
|
this.previewExerciseHtml = '';
|
|
this.previewQuizData = [];
|
|
this.previewSlides = [];
|
|
this.previewActiveTabs = [];
|
|
this.previewError = null;
|
|
return;
|
|
}
|
|
this.previewLoading = true;
|
|
try {
|
|
const res = renderMarkdownPreview(this.body);
|
|
if (res.success) {
|
|
this.previewHtml = res.lesson_content ?? '';
|
|
this.previewExerciseHtml = res.exercise_content ?? '';
|
|
this.previewQuizData = res.quiz_data ?? [];
|
|
this.previewSlides = res.slides ?? [];
|
|
this.previewActiveTabs = res.active_tabs ?? [];
|
|
this.previewError = null;
|
|
} else {
|
|
this.previewError = res.message || 'Preview gagal';
|
|
}
|
|
} catch (e: any) {
|
|
this.previewError = e.message || 'Gagal merender preview';
|
|
} finally {
|
|
this.previewLoading = false;
|
|
}
|
|
}
|
|
|
|
async handleSave() {
|
|
if (!this.activePath || this.saving) return;
|
|
this.saving = true;
|
|
try {
|
|
const res = await saveDraft(this.activePath, this.body, this.baseMtime);
|
|
if (res.success) {
|
|
this.draftId = res.draft_id;
|
|
this.dirty = false;
|
|
this.showMessage('success', 'Draft tersimpan');
|
|
} else {
|
|
this.showMessage('error', res.message || 'Gagal menyimpan draft');
|
|
}
|
|
} catch {
|
|
this.showMessage('error', 'Gagal terhubung ke server');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
}
|
|
|
|
async handlePublish() {
|
|
if (!this.activePath || this.publishing) return;
|
|
this.publishing = true;
|
|
try {
|
|
if (this.dirty || !this.draftId) {
|
|
const saveRes = await saveDraft(this.activePath, this.body, this.baseMtime);
|
|
if (!saveRes.success) {
|
|
this.showMessage('error', saveRes.message || 'Gagal menyimpan draft sebelum publish');
|
|
return;
|
|
}
|
|
this.draftId = saveRes.draft_id;
|
|
this.dirty = false;
|
|
}
|
|
|
|
if (!this.draftId) {
|
|
this.showMessage('error', 'Draft ID tidak ditemukan untuk dipublikasikan');
|
|
return;
|
|
}
|
|
|
|
const res = await publishDraft(this.draftId);
|
|
if (res.success) {
|
|
this.dirty = false;
|
|
this.showMessage('success', `Berhasil dipublikasikan ke ${res.published_path}`);
|
|
// Reload to get fresh base_mtime
|
|
if (this.activePath) {
|
|
await this.loadDraft(this.activePath);
|
|
}
|
|
} else {
|
|
if (res.message === 'conflict') {
|
|
this.showMessage('error', res.detail || 'Konflik: file berubah di server. Muat ulang terlebih dahulu.');
|
|
} else {
|
|
this.showMessage('error', res.message || 'Gagal mempublikasikan');
|
|
}
|
|
}
|
|
} catch {
|
|
this.showMessage('error', 'Gagal terhubung ke server');
|
|
} finally {
|
|
this.publishing = false;
|
|
}
|
|
}
|
|
|
|
private showMessage(type: 'success' | 'error', text: string) {
|
|
this.lastMessage = { type, text };
|
|
setTimeout(() => { this.lastMessage = null; }, 4000);
|
|
}
|
|
}
|