2026-08-21 15:55:21 +07:00
|
|
|
export interface TreeNode {
|
|
|
|
|
type: 'file' | 'folder';
|
|
|
|
|
name: string;
|
|
|
|
|
path: string;
|
|
|
|
|
ext?: string;
|
|
|
|
|
children?: TreeNode[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DraftResponse {
|
|
|
|
|
success: boolean;
|
|
|
|
|
source?: 'draft' | 'file';
|
|
|
|
|
draft_id: string | null;
|
|
|
|
|
body?: string;
|
|
|
|
|
base_mtime?: number | null;
|
|
|
|
|
message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface PreviewResponse {
|
|
|
|
|
success: boolean;
|
|
|
|
|
lesson_content?: string;
|
|
|
|
|
exercise_content?: string;
|
|
|
|
|
quiz_data?: unknown[];
|
|
|
|
|
slides?: string[];
|
|
|
|
|
active_tabs?: string[];
|
|
|
|
|
message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getTree(root: 'content' | 'assets'): Promise<TreeNode[]> {
|
|
|
|
|
const res = await fetch(`/api/content/tree?root=${root}`, { credentials: 'include' });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
return data.success ? data.tree : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDraft(targetPath: string): Promise<DraftResponse> {
|
|
|
|
|
const res = await fetch(`/api/content/drafts?target_path=${encodeURIComponent(targetPath)}`, {
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function saveDraft(targetPath: string, body: string, baseMtime: number | null) {
|
|
|
|
|
const res = await fetch('/api/content/drafts', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ target_path: targetPath, body, base_mtime: baseMtime }),
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:32:28 +07:00
|
|
|
/**
|
|
|
|
|
* @deprecated Use client-side renderMarkdownPreview from './markdown' instead.
|
|
|
|
|
* Kept for fallback or direct API verification purposes.
|
|
|
|
|
*/
|
2026-08-21 15:55:21 +07:00
|
|
|
export async function previewContent(body: string): Promise<PreviewResponse> {
|
|
|
|
|
const res = await fetch('/api/content/preview', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ body }),
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function publishDraft(draftId: string) {
|
|
|
|
|
const res = await fetch(`/api/content/drafts/${draftId}/publish`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function discardDraft(draftId: string) {
|
|
|
|
|
const res = await fetch(`/api/content/drafts/${draftId}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createFolder(root: 'content' | 'assets', path: string) {
|
|
|
|
|
const res = await fetch('/api/content/tree/folder', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ root, path }),
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createFile(path: string, template?: string) {
|
|
|
|
|
const res = await fetch('/api/content/tree/file', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ root: 'content', path, template }),
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 08:18:02 +07:00
|
|
|
export async function renameEntry(
|
|
|
|
|
root: 'content' | 'assets',
|
|
|
|
|
oldPath: string,
|
|
|
|
|
newPath: string,
|
|
|
|
|
confirmCritical = false,
|
|
|
|
|
) {
|
2026-08-21 15:55:21 +07:00
|
|
|
const res = await fetch('/api/content/tree/rename', {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
2026-08-23 08:18:02 +07:00
|
|
|
body: JSON.stringify({ root, old_path: oldPath, new_path: newPath, confirm_critical: confirmCritical }),
|
2026-08-21 15:55:21 +07:00
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 08:18:02 +07:00
|
|
|
export async function deleteEntry(root: 'content' | 'assets', path: string, confirmCritical = false, force = false) {
|
2026-08-21 15:55:21 +07:00
|
|
|
const res = await fetch(
|
2026-08-23 08:18:02 +07:00
|
|
|
`/api/content/tree/entry?root=${root}&path=${encodeURIComponent(path)}&force=${force}&confirm_critical=${confirmCritical}`,
|
2026-08-21 15:55:21 +07:00
|
|
|
{ method: 'DELETE', credentials: 'include' },
|
|
|
|
|
);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileToBase64(file: File): Promise<string> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
const result = reader.result as string;
|
|
|
|
|
resolve(result.split(',')[1] ?? '');
|
|
|
|
|
};
|
|
|
|
|
reader.onerror = () => reject(reader.error);
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadAsset(file: File, folder = '') {
|
|
|
|
|
const content_base64 = await fileToBase64(file);
|
|
|
|
|
const res = await fetch('/api/content/assets/upload', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ filename: file.name, folder, content_base64 }),
|
|
|
|
|
});
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|