"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { useYjsDocument } from "@/hooks/use-yjs-document"; import { usePresence } from "@/hooks/use-presence"; import { PresenceAvatars, EditingIndicator } from "@/components/presence-avatars"; import { TestPanel } from "@/components/test-panel"; import { Save, FileText, Check, Loader2, Wifi, WifiOff, CloudOff, Circle, RefreshCw, AlertCircle, } from "lucide-react"; import * as Y from "yjs"; export function DocumentEditor() { const { documentId, title, content, isLoading, saveStatus, lastSaved, unsyncedCount, isOnline, updateTitle, updateContent, manualSave, getYDoc, getStateVector, // Tab sync status isTabSyncActive, connectedTabs, tabId, syncStatus, requestReconciliation, flushOfflineQueue, } = useYjsDocument(); // Presence for collaboration const { currentUser, remoteUsers, updateCursor, updateSelection, clearEditing, } = usePresence({ documentId, enabled: true, }); const contentRef = useRef(null); const titleRef = useRef(null); // Simulated offline state for testing const [simulatedOffline, setSimulatedOffline] = useState(false); const effectiveOnline = isOnline && !simulatedOffline; const handleTitleChange = (e: React.ChangeEvent) => { updateTitle(e.target.value); }; const handleContentChange = (e: React.ChangeEvent) => { const cursorPosition = e.target.selectionStart; updateContent(e.target.value, cursorPosition); }; // Track cursor/selection for presence const handleTitleFocus = () => { if (titleRef.current) { updateCursor(titleRef.current.selectionStart, "title"); } }; const handleTitleSelect = () => { if (titleRef.current) { updateSelection( titleRef.current.selectionStart, titleRef.current.selectionEnd, "title" ); } }; const handleContentFocus = () => { if (contentRef.current) { updateCursor(contentRef.current.selectionStart, "content"); } }; const handleContentSelect = () => { if (contentRef.current) { updateSelection( contentRef.current.selectionStart, contentRef.current.selectionEnd, "content" ); } }; const handleBlur = () => { clearEditing(); }; // Keyboard shortcut for save useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); manualSave(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [manualSave]); // Test panel callbacks const handleSimulateOffline = useCallback(() => { setSimulatedOffline(true); }, []); const handleSimulateOnline = useCallback(() => { setSimulatedOffline(false); // Trigger reconciliation setTimeout(() => { requestReconciliation(); flushOfflineQueue(); }, 100); }, [requestReconciliation, flushOfflineQueue]); const handleForceSync = useCallback(() => { requestReconciliation(); flushOfflineQueue(); }, [requestReconciliation, flushOfflineQueue]); const handleClearLocalData = useCallback(async () => { // Clear IndexedDB const dbs = await indexedDB.databases(); for (const db of dbs) { if (db.name) { indexedDB.deleteDatabase(db.name); } } // Clear localStorage localStorage.clear(); // Reload window.location.reload(); }, []); const handleDuplicateUpdate = useCallback(() => { // Send a duplicate/redundant update to test idempotency const ydoc = getYDoc(); if (ydoc) { const state = Y.encodeStateAsUpdate(ydoc); Y.applyUpdate(ydoc, state, "test-duplicate"); } }, [getYDoc]); const getStateVectorCallback = useCallback(() => { return getStateVector(); }, [getStateVector]); if (isLoading) { return (
); } const getSyncStatusBadge = () => { if (simulatedOffline) { return (
Simulated Offline
); } switch (syncStatus.state) { case "offline": return (
Offline
); case "syncing": return (
Syncing...
); case "reconnecting": return (
Reconnecting...
); case "diverged": return (
Diverged
); case "synced": default: if (connectedTabs > 0) { return (
Live
); } return null; } }; return (
{/* Header */}
{/* Presence Avatars */} {/* Sync status */}
{getSyncStatusBadge()}
{/* Online/Offline */}
{effectiveOnline ? ( <> Online ) : ( <> Offline )}
{/* Pending updates */} {(syncStatus.pendingUpdates > 0 || unsyncedCount > 0) && (
{syncStatus.pendingUpdates || unsyncedCount} pending
)} {/* CRDT badge */}
CRDT
{/* Save status */}
{saveStatus === "saving" && ( <> Saving... )} {saveStatus === "saved" && ( <> Saved )} {saveStatus === "idle" && lastSaved && ( {lastSaved.toLocaleTimeString()} )}
{/* Save button */}
{/* Sync Banner */} {isTabSyncActive && connectedTabs > 0 && syncStatus.state === "synced" && !simulatedOffline && (
Real-time sync with {connectedTabs} other{" "} {connectedTabs === 1 ? "tab" : "tabs"} {syncStatus.lastSyncTime && ( - Last sync: {syncStatus.lastSyncTime.toLocaleTimeString()} )}
)} {/* Offline Banner */} {(!effectiveOnline || simulatedOffline) && (
{simulatedOffline ? "Simulated offline mode. " : "You are offline. "} Changes are saved locally and will sync when you reconnect. {syncStatus.pendingUpdates > 0 && ( ({syncStatus.pendingUpdates} updates queued) )}
)} {/* Reconnecting Banner */} {syncStatus.state === "reconnecting" && !simulatedOffline && (
Reconnecting and syncing missed updates...
)} {/* Editor */}