146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { toast } from "sonner";
|
|
import * as api from "@/api/client";
|
|
import { autocompletion } from "@codemirror/autocomplete";
|
|
import { useEditorStore } from "@/stores/editorStore";
|
|
import { usePagesStore } from "@/stores/pagesStore";
|
|
import { useUnsavedGuard } from "@/hooks/useUnsavedGuard";
|
|
import { useCompile } from "@/hooks/useCompile";
|
|
import { useKeyboardSave } from "@/hooks/useKeyboardSave";
|
|
import { uframeHighlight } from "@/components/editor/uframeHighlight";
|
|
import { uframeCommandSource, uframeValueHintSource, loadCommandsFromApi } from "@/components/editor/uframeCommands";
|
|
import { keywordHoverTooltip } from "@/components/editor/uframeHover";
|
|
import EditorPointer from "@/components/editor/EditorPointer";
|
|
import PreviewPane from "@/components/editor/PreviewPane";
|
|
import SourcePane from "@/components/editor/SourcePane";
|
|
import ToolBar from "@/components/editor/ToolBar";
|
|
import {
|
|
ResizablePanelGroup,
|
|
ResizablePanel,
|
|
ResizableHandle,
|
|
} from "@/components/ui/resizable";
|
|
|
|
export default function EditorView() {
|
|
const { name } = useParams<{ name: string }>();
|
|
const navigate = useNavigate();
|
|
const isNew = !name;
|
|
|
|
const ufSource = useEditorStore((s) => s.ufSource);
|
|
const isDirty = useEditorStore((s) => s.isDirty);
|
|
const setSource = useEditorStore((s) => s.setSource);
|
|
const setDirty = useEditorStore((s) => s.setDirty);
|
|
const setCurrentPage = useEditorStore((s) => s.setCurrentPage);
|
|
const reset = useEditorStore((s) => s.reset);
|
|
|
|
const { fetchPages } = usePagesStore();
|
|
const [pageName, setPageName] = useState(name ?? "");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// µFrame extensions: syntax highlighting + slash commands
|
|
const extensions = useMemo(
|
|
() => [
|
|
...uframeHighlight(),
|
|
autocompletion({
|
|
override: [uframeCommandSource, uframeValueHintSource],
|
|
icons: false,
|
|
activateOnTyping: true,
|
|
}),
|
|
keywordHoverTooltip,
|
|
],
|
|
[],
|
|
);
|
|
|
|
// Load DSL commands from backend registry on mount
|
|
useEffect(() => {
|
|
loadCommandsFromApi();
|
|
}, []);
|
|
|
|
// Auto-compile on source changes
|
|
useCompile();
|
|
useUnsavedGuard();
|
|
|
|
// Fetch pages for backlinks
|
|
useEffect(() => {
|
|
fetchPages();
|
|
}, []);
|
|
|
|
// Load page on mount / route change
|
|
useEffect(() => {
|
|
reset();
|
|
if (name) {
|
|
setPageName(name);
|
|
api.fetchPage(name).then((data) => {
|
|
if (data.source != null) {
|
|
setSource(data.source);
|
|
setDirty(false);
|
|
}
|
|
});
|
|
}
|
|
return () => reset();
|
|
}, [name]);
|
|
|
|
const handleSave = useCallback(
|
|
async (publish: boolean) => {
|
|
const slug = pageName
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_-]/g, "-");
|
|
if (!slug) {
|
|
toast.error("Enter a page name.");
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
const meta = await api.savePage(slug, ufSource, publish);
|
|
setCurrentPage(meta);
|
|
setDirty(false);
|
|
fetchPages();
|
|
toast.success(publish ? "Published" : "Draft saved");
|
|
if (isNew) navigate(`/editor/${slug}`, { replace: true });
|
|
} catch (e) {
|
|
toast.error(`Save failed: ${e}`);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[pageName, ufSource, isNew, navigate],
|
|
);
|
|
|
|
useKeyboardSave(
|
|
useCallback(() => handleSave(false), [handleSave]),
|
|
useCallback(() => handleSave(true), [handleSave]),
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<EditorPointer />
|
|
<div className="flex flex-col flex-1 min-h-0">
|
|
<ToolBar
|
|
pageName={pageName}
|
|
onNameChange={isNew ? setPageName : undefined}
|
|
onSaveDraft={() => handleSave(false)}
|
|
onPublish={() => handleSave(true)}
|
|
saving={saving}
|
|
isDirty={isDirty}
|
|
/>
|
|
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0">
|
|
<ResizablePanel defaultSize={50} minSize={20}>
|
|
<SourcePane
|
|
ufSource={ufSource}
|
|
setSource={setSource}
|
|
extensions={extensions}
|
|
/>
|
|
</ResizablePanel>
|
|
<ResizableHandle />
|
|
<ResizablePanel defaultSize={50} minSize={20}>
|
|
<PreviewPane />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|