);
}
+
+
+/** Per-row action menu for a page. */
+function PageActions({
+ name,
+ published,
+ onPublish,
+ onUnpublish,
+ onDelete,
+}: {
+ name: string;
+ published: boolean;
+ onPublish: () => void;
+ onUnpublish: () => void;
+ onDelete: () => void;
+}) {
+ const navigate = useNavigate();
+
+ return (
+
+ e.stopPropagation()}
+ className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
+ >
+
+
+ }
+ />
+
+
+ {published ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/routes/EditorView.tsx b/frontend/src/routes/EditorView.tsx
index 4ec1d7e..11d3a2e 100644
--- a/frontend/src/routes/EditorView.tsx
+++ b/frontend/src/routes/EditorView.tsx
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { BookOpen, Upload } from "lucide-react";
+import * as api from "@/api/client";
import { autocompletion } from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";
import { useEditorStore } from "@/stores/editorStore";
@@ -77,17 +78,14 @@ export default function EditorView() {
reset();
if (name) {
setPageName(name);
- fetch(`/api/pages/${name}`)
- .then((r) => r.json())
- .then((data) => {
- if (data.source != null) {
- useEditorStore.setState({
- ufSource: data.source,
- isDirty: false,
- currentPage: data,
- });
- }
- });
+ api.fetchPage(name).then((data) => {
+ if (data.source != null) {
+ useEditorStore.setState({
+ ufSource: data.source,
+ isDirty: false,
+ });
+ }
+ });
}
return () => reset();
}, [name]);
@@ -104,13 +102,7 @@ export default function EditorView() {
}
setSaving(true);
try {
- const res = await fetch(`/api/pages/${slug}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ source: ufSource, publish }),
- });
- if (!res.ok) throw new Error(await res.text());
- const meta = await res.json();
+ const meta = await api.savePage(slug, ufSource, publish);
setCurrentPage(meta);
setDirty(false);
fetchPages();
@@ -189,11 +181,7 @@ function SourcePane({
if (!file) return;
setUploading(true);
try {
- const form = new FormData();
- form.append("file", file);
- const res = await fetch("/api/upload-image", { method: "POST", body: form });
- if (!res.ok) throw new Error(await res.text());
- const data = await res.json();
+ const data = await api.uploadImage(file);
toast.success(`Uploaded ${data.filename}`);
setSource(`image "${data.path}" braille 30\n align center`);
} catch (err) {
diff --git a/frontend/src/routes/GraphView.tsx b/frontend/src/routes/GraphView.tsx
deleted file mode 100644
index 57cf492..0000000
--- a/frontend/src/routes/GraphView.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { useMemo } from "react";
-import { useNavigate } from "react-router-dom";
-import {
- ReactFlow,
- Background,
- Controls,
- type Node,
- type Edge,
-} from "@xyflow/react";
-import dagre from "@dagrejs/dagre";
-import { useGraph } from "@/hooks/useGraph";
-import "@xyflow/react/dist/style.css";
-
-const NODE_WIDTH = 160;
-const NODE_HEIGHT = 50;
-
-function layoutGraph(
- nodes: Node[],
- edges: Edge[]
-): { nodes: Node[]; edges: Edge[] } {
- const g = new dagre.graphlib.Graph();
- g.setDefaultEdgeLabel(() => ({}));
- g.setGraph({ rankdir: "TB", nodesep: 50, ranksep: 80 });
-
- nodes.forEach((n) =>
- g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT })
- );
- edges.forEach((e) => g.setEdge(e.source, e.target));
- dagre.layout(g);
-
- const laid = nodes.map((n) => {
- const pos = g.node(n.id);
- return {
- ...n,
- position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 },
- };
- });
-
- return { nodes: laid, edges };
-}
-
-export default function GraphView() {
- const { data, loading } = useGraph();
- const navigate = useNavigate();
-
- const { nodes, edges } = useMemo(() => {
- if (!data) return { nodes: [], edges: [] };
-
- const rfNodes: Node[] = data.nodes.map((n) => ({
- id: n.id,
- data: { label: n.title ?? n.id },
- position: { x: 0, y: 0 },
- style: {
- background: n.published
- ? "oklch(0.488 0.14 145)"
- : "oklch(0.7 0.15 80)",
- color: "#fff",
- border:
- n.id === "index"
- ? "2px solid oklch(0.6 0.2 250)"
- : "1px solid oklch(1 0 0 / 10%)",
- borderRadius: 8,
- padding: "8px 16px",
- fontSize: 13,
- fontWeight: n.id === "index" ? 700 : 400,
- width: NODE_WIDTH,
- },
- }));
-
- const rfEdges: Edge[] = data.edges.map((e, i) => ({
- id: `e-${i}`,
- source: e.source,
- target: e.target,
- style: { stroke: "oklch(0.556 0 0)" },
- }));
-
- return layoutGraph(rfNodes, rfEdges);
- }, [data]);
-
- if (loading)
- return (
-
- Loading graph...
-
- );
-
- return (
-
- navigate(`/editor/${node.id}`)}
- fitView
- proOptions={{ hideAttribution: true }}
- >
-
-
-
-
- );
-}
diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts
index e287e25..4969fec 100644
--- a/frontend/src/stores/editorStore.ts
+++ b/frontend/src/stores/editorStore.ts
@@ -1,13 +1,5 @@
import { create } from "zustand";
-
-export interface PageMeta {
- name: string;
- title: string | null;
- published: boolean;
- has_source: boolean;
- last_modified: number | null;
- size: number | null;
-}
+import type { PageMeta } from "@/api/client";
interface EditorStore {
// Source
diff --git a/frontend/src/stores/pagesStore.ts b/frontend/src/stores/pagesStore.ts
index 9f91fd6..138aada 100644
--- a/frontend/src/stores/pagesStore.ts
+++ b/frontend/src/stores/pagesStore.ts
@@ -1,11 +1,12 @@
import { create } from "zustand";
-import type { PageMeta } from "@/stores/editorStore";
+import * as api from "@/api/client";
interface PagesStore {
- pages: PageMeta[];
+ pages: api.PageMeta[];
isLoading: boolean;
fetchPages: () => Promise;
deletePage: (name: string) => Promise;
+ publishPage: (name: string) => Promise;
unpublishPage: (name: string) => Promise;
}
@@ -16,8 +17,7 @@ export const usePagesStore = create((set, get) => ({
fetchPages: async () => {
set({ isLoading: true });
try {
- const res = await fetch("/api/pages");
- const pages = await res.json();
+ const pages = await api.fetchPages();
set({ pages });
} finally {
set({ isLoading: false });
@@ -25,20 +25,22 @@ export const usePagesStore = create((set, get) => ({
},
deletePage: async (name: string) => {
- await fetch(`/api/pages/${name}`, { method: "DELETE" });
+ await api.deletePage(name);
+ await get().fetchPages();
+ },
+
+ publishPage: async (name: string) => {
+ const page = await api.fetchPage(name);
+ if (page.source) {
+ await api.savePage(name, page.source, true);
+ }
await get().fetchPages();
},
unpublishPage: async (name: string) => {
- // Fetch current source, re-save as draft only
- const res = await fetch(`/api/pages/${name}`);
- const data = await res.json();
- if (data.source) {
- await fetch(`/api/pages/${name}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ source: data.source, publish: false }),
- });
+ const page = await api.fetchPage(name);
+ if (page.source) {
+ await api.savePage(name, page.source, false);
}
await get().fetchPages();
},