feat: implement save and leave functionality in editor, enhance layout and error handling

This commit is contained in:
Anish Sarkar 2026-02-17 12:42:50 +05:30
parent 664961076c
commit 6cc74689bc
2 changed files with 56 additions and 62 deletions

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { AlertCircle, ArrowLeft, FileText, Save } from "lucide-react"; import { AlertCircle, ArrowLeft, FileText } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@ -297,6 +297,12 @@ export default function EditorPage() {
} }
}; };
const handleSaveAndLeave = async () => {
setShowUnsavedDialog(false);
setPendingNavigation(null);
await handleSave();
};
const handleCancelLeave = () => { const handleCancelLeave = () => {
setShowUnsavedDialog(false); setShowUnsavedDialog(false);
setPendingNavigation(null); setPendingNavigation(null);
@ -364,11 +370,21 @@ export default function EditorPage() {
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="flex flex-col min-h-screen w-full" className="flex flex-col h-screen w-full overflow-hidden"
> >
{/* Toolbar */} {/* Toolbar */}
<div className="sticky top-0 z-40 flex h-14 md:h-16 shrink-0 items-center gap-2 md:gap-4 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-3 md:px-6"> <div className="flex h-14 md:h-16 shrink-0 items-center border-b bg-background pl-1.5 pr-3 md:pl-3 md:pr-6">
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0"> <div className="flex items-center gap-1.5 md:gap-2 flex-1 min-w-0">
<Button
variant="ghost"
size="icon"
onClick={handleBack}
disabled={saving}
className="h-7 w-7 shrink-0 p-0"
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Back</span>
</Button>
<FileText className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground shrink-0" /> <FileText className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1> <h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1>
@ -377,53 +393,23 @@ export default function EditorPage() {
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleBack}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
<ArrowLeft className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">Back</span>
</Button>
<Button
onClick={handleSave}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
{saving ? (
<>
<Spinner size="sm" className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">{isNewNote ? "Creating" : "Saving"}</span>
</>
) : (
<>
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">Save</span>
</>
)}
</Button>
</div>
</div> </div>
{/* Editor Container */} {/* Editor Container */}
<div className="flex-1 min-h-0 overflow-hidden relative"> <div className="flex-1 min-h-0 flex flex-col overflow-hidden relative">
<div className="h-full w-full overflow-auto p-3 md:p-6">
{error && ( {error && (
<motion.div <motion.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="mb-6 max-w-4xl mx-auto" className="px-3 md:px-6 pt-3 md:pt-6"
> >
<div className="flex items-center gap-2 p-4 rounded-lg border border-destructive/50 bg-destructive/10 text-destructive"> <div className="flex items-center gap-2 p-4 rounded-lg border border-destructive/50 bg-destructive/10 text-destructive max-w-4xl mx-auto">
<AlertCircle className="h-5 w-5 shrink-0" /> <AlertCircle className="h-5 w-5 shrink-0" />
<p className="text-sm">{error}</p> <p className="text-sm">{error}</p>
</div> </div>
</motion.div> </motion.div>
)} )}
<div className="max-w-4xl mx-auto"> <div className="flex-1 min-h-0">
<PlateEditor <PlateEditor
key={documentId} key={documentId}
markdown={document?.source_markdown ?? ""} markdown={document?.source_markdown ?? ""}
@ -431,10 +417,10 @@ export default function EditorPage() {
onSave={handleSave} onSave={handleSave}
hasUnsavedChanges={hasUnsavedChanges} hasUnsavedChanges={hasUnsavedChanges}
isSaving={saving} isSaving={saving}
defaultEditing={true}
/> />
</div> </div>
</div> </div>
</div>
{/* Unsaved Changes Dialog */} {/* Unsaved Changes Dialog */}
<AlertDialog <AlertDialog
@ -452,7 +438,12 @@ export default function EditorPage() {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={handleCancelLeave}>Cancel</AlertDialogCancel> <AlertDialogCancel onClick={handleCancelLeave}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirmLeave}>OK</AlertDialogAction> <AlertDialogAction onClick={handleSaveAndLeave}>
Save
</AlertDialogAction>
<AlertDialogAction onClick={handleConfirmLeave} className="border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground">
Leave without saving
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View file

@ -50,6 +50,8 @@ interface PlateEditorProps {
hasUnsavedChanges?: boolean; hasUnsavedChanges?: boolean;
/** Whether a save is in progress */ /** Whether a save is in progress */
isSaving?: boolean; isSaving?: boolean;
/** Start the editor in editing mode instead of viewing mode. Ignored when readOnly is true. */
defaultEditing?: boolean;
} }
export function PlateEditor({ export function PlateEditor({
@ -63,14 +65,15 @@ export function PlateEditor({
onSave, onSave,
hasUnsavedChanges = false, hasUnsavedChanges = false,
isSaving = false, isSaving = false,
defaultEditing = false,
}: PlateEditorProps) { }: PlateEditorProps) {
const lastMarkdownRef = useRef(markdown); const lastMarkdownRef = useRef(markdown);
// Always initialize the editor in readOnly mode (viewing mode). // When readOnly is forced, always start in readOnly.
// For non-forced readOnly, the user can toggle to editing via ModeToolbarButton. // Otherwise, respect defaultEditing to decide initial mode.
// For forced readOnly, the mode button is hidden and readOnly stays true. // The user can still toggle between editing/viewing via ModeToolbarButton.
const editor = usePlateEditor({ const editor = usePlateEditor({
readOnly: true, readOnly: readOnly || !defaultEditing,
plugins: [ plugins: [
...BasicNodesKit, ...BasicNodesKit,
...TableKit, ...TableKit,