tfrere HF Staff commited on
Commit
6df81e7
·
1 Parent(s): 7bafae7

feat: add publish button with confirmation dialog

Browse files

- Purple publish icon in the top bar
- Confirmation dialog explaining what publish does
- Loading state with spinner during publish
- Success/error states with appropriate messaging
- Calls POST /api/publish with current docName

Made-with: Cursor

Files changed (1) hide show
  1. frontend/src/App.tsx +96 -0
frontend/src/App.tsx CHANGED
@@ -14,6 +14,16 @@ import UndoIcon from "@mui/icons-material/Undo";
14
  import RedoIcon from "@mui/icons-material/Redo";
15
  import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
16
  import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
 
 
 
 
 
 
 
 
 
 
17
  import { Editor } from "./editor/Editor";
18
  import { CommentsSidebar } from "./components/CommentsSidebar";
19
  import { CommentDialog } from "./components/CommentDialog";
@@ -61,6 +71,9 @@ export default function App() {
61
  const [showExport, setShowExport] = useState(false);
62
  const [commentDialogOpen, setCommentDialogOpen] = useState(false);
63
  const [settingsOpen, setSettingsOpen] = useState(false);
 
 
 
64
  const selectionRange = useRef<{ from: number; to: number } | null>(null);
65
  const [hasSelection, setHasSelection] = useState(false);
66
  const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
@@ -107,6 +120,26 @@ export default function App() {
107
  setSettingsMap(map);
108
  }, []);
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  const handleAddComment = useCallback(() => {
111
  const editor = editorRef.current;
112
  if (!editor) return;
@@ -195,6 +228,16 @@ export default function App() {
195
  <FileDownloadOutlinedIcon sx={{ fontSize: 18 }} />
196
  </IconButton>
197
  </Tooltip>
 
 
 
 
 
 
 
 
 
 
198
  <Chip
199
  label={user.name}
200
  size="small"
@@ -316,6 +359,59 @@ export default function App() {
316
  store={frontmatterStore}
317
  settingsMap={settingsMap}
318
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </Box>
320
  );
321
  }
 
14
  import RedoIcon from "@mui/icons-material/Redo";
15
  import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
16
  import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
17
+ import PublishOutlinedIcon from "@mui/icons-material/PublishOutlined";
18
+ import {
19
+ Button,
20
+ Dialog,
21
+ DialogTitle,
22
+ DialogContent,
23
+ DialogContentText,
24
+ DialogActions,
25
+ CircularProgress,
26
+ } from "@mui/material";
27
  import { Editor } from "./editor/Editor";
28
  import { CommentsSidebar } from "./components/CommentsSidebar";
29
  import { CommentDialog } from "./components/CommentDialog";
 
71
  const [showExport, setShowExport] = useState(false);
72
  const [commentDialogOpen, setCommentDialogOpen] = useState(false);
73
  const [settingsOpen, setSettingsOpen] = useState(false);
74
+ const [publishDialogOpen, setPublishDialogOpen] = useState(false);
75
+ const [publishState, setPublishState] = useState<"idle" | "loading" | "success" | "error">("idle");
76
+ const [publishError, setPublishError] = useState("");
77
  const selectionRange = useRef<{ from: number; to: number } | null>(null);
78
  const [hasSelection, setHasSelection] = useState(false);
79
  const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
 
120
  setSettingsMap(map);
121
  }, []);
122
 
123
+ const handlePublish = useCallback(async () => {
124
+ setPublishState("loading");
125
+ setPublishError("");
126
+ try {
127
+ const res = await fetch("/api/publish", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ docName }),
131
+ });
132
+ if (!res.ok) {
133
+ const data = await res.json().catch(() => ({ error: "Publish failed" }));
134
+ throw new Error(data.error || "Publish failed");
135
+ }
136
+ setPublishState("success");
137
+ } catch (err: any) {
138
+ setPublishState("error");
139
+ setPublishError(err.message || "Unknown error");
140
+ }
141
+ }, [docName]);
142
+
143
  const handleAddComment = useCallback(() => {
144
  const editor = editorRef.current;
145
  if (!editor) return;
 
228
  <FileDownloadOutlinedIcon sx={{ fontSize: 18 }} />
229
  </IconButton>
230
  </Tooltip>
231
+ <Box sx={{ width: "1px", height: 16, bgcolor: "divider", mx: 0.5 }} />
232
+ <Tooltip title="Publish article" arrow>
233
+ <IconButton
234
+ size="small"
235
+ onClick={() => { setPublishState("idle"); setPublishDialogOpen(true); }}
236
+ sx={{ color: "primary.main" }}
237
+ >
238
+ <PublishOutlinedIcon sx={{ fontSize: 18 }} />
239
+ </IconButton>
240
+ </Tooltip>
241
  <Chip
242
  label={user.name}
243
  size="small"
 
359
  store={frontmatterStore}
360
  settingsMap={settingsMap}
361
  />
362
+
363
+ <Dialog
364
+ open={publishDialogOpen}
365
+ onClose={() => setPublishDialogOpen(false)}
366
+ maxWidth="xs"
367
+ fullWidth
368
+ >
369
+ <DialogTitle>
370
+ {publishState === "success" ? "Published!" : "Publish article"}
371
+ </DialogTitle>
372
+ <DialogContent>
373
+ {publishState === "idle" && (
374
+ <DialogContentText>
375
+ This will generate a static HTML page, PDF, and thumbnail
376
+ from the current document. Visitors without edit access
377
+ will see the published version.
378
+ </DialogContentText>
379
+ )}
380
+ {publishState === "loading" && (
381
+ <Box sx={{ display: "flex", alignItems: "center", gap: 2, py: 2 }}>
382
+ <CircularProgress size={24} />
383
+ <DialogContentText sx={{ m: 0 }}>
384
+ Publishing...
385
+ </DialogContentText>
386
+ </Box>
387
+ )}
388
+ {publishState === "success" && (
389
+ <DialogContentText>
390
+ Article published successfully. Visitors will now see
391
+ the updated version.
392
+ </DialogContentText>
393
+ )}
394
+ {publishState === "error" && (
395
+ <DialogContentText color="error">
396
+ {publishError || "An error occurred while publishing."}
397
+ </DialogContentText>
398
+ )}
399
+ </DialogContent>
400
+ <DialogActions>
401
+ {publishState === "idle" && (
402
+ <>
403
+ <Button onClick={() => setPublishDialogOpen(false)}>Cancel</Button>
404
+ <Button variant="contained" onClick={handlePublish}>
405
+ Publish
406
+ </Button>
407
+ </>
408
+ )}
409
+ {publishState === "loading" && null}
410
+ {(publishState === "success" || publishState === "error") && (
411
+ <Button onClick={() => setPublishDialogOpen(false)}>Close</Button>
412
+ )}
413
+ </DialogActions>
414
+ </Dialog>
415
  </Box>
416
  );
417
  }