"use client"; import { Toaster } from "@mdxui/primitives/sonner"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { toast } from "sonner"; import { useAutosave } from "../hooks/use-autosave"; import type { EditorPanelProps } from "./types"; import { type EditorContextValue, type EditorHandle, EditorProvider, } from "./editor-context"; import { EditorDrawer } from "./editor-drawer"; import { EditorTrigger } from "./editor-trigger"; export function EditorPanel({ path = "", initialContent = "", onSave, shortcut = "meta+i", showTrigger = true, }: EditorPanelProps) { const [isOpen, setIsOpen] = useState(false); const [content, setContent] = useState(initialContent); const [savedContent, setSavedContent] = useState(initialContent); const [isSaving, setIsSaving] = useState(false); // Ref for editor handle (jump-to-line, etc.) const editorRef = useRef(null); const isDirty = content !== savedContent; // Autosave hook const { showRecoveryPrompt, recoveredData, clearAutosave, handleRestore, handleDiscardRecovery, } = useAutosave(path, content, isDirty, initialContent); // Handle recovery restoration const onRestoreRecovery = useCallback(() => { const restored = handleRestore(); if (restored) { setContent(restored); toast.success("Changes restored"); } }, [handleRestore]); // Handle recovery dismissal const onDismissRecovery = useCallback(() => { handleDiscardRecovery(); toast("Recovered changes discarded"); }, [handleDiscardRecovery]); // Toggle shortcut useHotkeys( shortcut, () => setIsOpen((prev) => !prev), { enableOnFormTags: false, preventDefault: true, }, [shortcut], ); // Save shortcut (when open) useHotkeys( "meta+s", async (e) => { if (!isOpen || !isDirty) return; e.preventDefault(); await handleSave(); }, { enableOnFormTags: true, enabled: isOpen, }, [isOpen, isDirty, content], ); // Save handler const handleSave = useCallback(async () => { if (!onSave || !isDirty) return; setIsSaving(true); try { await onSave(content); setSavedContent(content); clearAutosave(); toast.success("Saved successfully"); } catch (error) { toast.error("Failed to save", { description: "Check your connection and try again.", action: { label: "Retry", onClick: () => handleSave(), }, }); console.error("Save error:", error); } finally { setIsSaving(false); } }, [content, isDirty, onSave, clearAutosave]); // Close handler - just close, autosave protects user's work const handleClose = useCallback(() => { if (isDirty) { // Reset to saved content and notify user setContent(savedContent); toast("Changes discarded", { description: "Your work was auto-saved and can be restored next time.", }); } setIsOpen(false); }, [isDirty, savedContent]); // Sync content when initialContent changes (e.g., after external save) useEffect(() => { if (!isDirty) { setContent(initialContent); setSavedContent(initialContent); } }, [initialContent, isDirty]); // Memoize context value to prevent unnecessary re-renders const editorContextValue = useMemo( () => ({ content, onChange: setContent, path, isDirty, isSaving, onSave: handleSave, onClose: handleClose, editorRef, // Recovery banner state (only show if we have recovered data) recovery: showRecoveryPrompt && recoveredData ? { timestamp: recoveredData.timestamp, onRestore: onRestoreRecovery, onDismiss: onDismissRecovery, } : undefined, }), [ content, path, isDirty, isSaving, handleSave, handleClose, showRecoveryPrompt, recoveredData, onRestoreRecovery, onDismissRecovery, ], ); return ( <> {/* Toast container */} {/* Trigger button */} {showTrigger && ( (isOpen ? handleClose() : setIsOpen(true))} /> )} {/* Editor drawer with context provider */} ); }