react-embed-docs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- package/styles/docs.css +981 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { closestCenter, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core';
|
|
3
|
+
import { Plus } from 'lucide-react';
|
|
4
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { useReorderDocumentMutation } from '../hooks/useDocsQuery';
|
|
6
|
+
import { useDocument } from './DocumentProvider';
|
|
7
|
+
import { DragOverlayItem } from './DragOverlayItem';
|
|
8
|
+
import { SortableTreeItem } from './SortableTreeItem';
|
|
9
|
+
export function Sidebar({ isOpen, onToggle, documents, currentDocId }) {
|
|
10
|
+
const { onCreate, onOpen } = useDocument();
|
|
11
|
+
const [expandedFolders, setExpandedFolders] = useState(new Set());
|
|
12
|
+
const [activeId, setActiveId] = useState(null);
|
|
13
|
+
const [dropIndicator, setDropIndicator] = useState(null);
|
|
14
|
+
const reorderMutation = useReorderDocumentMutation();
|
|
15
|
+
const sensors = useSensors(useSensor(PointerSensor, {
|
|
16
|
+
activationConstraint: {
|
|
17
|
+
distance: 8,
|
|
18
|
+
},
|
|
19
|
+
}), useSensor(KeyboardSensor));
|
|
20
|
+
const toggleFolder = useCallback((folderId) => {
|
|
21
|
+
setExpandedFolders((prev) => {
|
|
22
|
+
const newExpanded = new Set(prev);
|
|
23
|
+
if (newExpanded.has(folderId)) {
|
|
24
|
+
newExpanded.delete(folderId);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
newExpanded.add(folderId);
|
|
28
|
+
}
|
|
29
|
+
return newExpanded;
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
// Resolve currentDocId (which might be a slug) to actual document ID
|
|
33
|
+
const resolvedCurrentDocId = useMemo(() => {
|
|
34
|
+
if (!currentDocId || documents.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
const byId = documents.find((d) => d.id === currentDocId);
|
|
37
|
+
if (byId)
|
|
38
|
+
return byId.id;
|
|
39
|
+
const bySlug = documents.find((d) => d.slug === currentDocId);
|
|
40
|
+
if (bySlug)
|
|
41
|
+
return bySlug.id;
|
|
42
|
+
return null;
|
|
43
|
+
}, [currentDocId, documents]);
|
|
44
|
+
// Build flattened tree for sortable context
|
|
45
|
+
const flattenedTree = useMemo(() => {
|
|
46
|
+
const items = [];
|
|
47
|
+
const processed = new Set();
|
|
48
|
+
const processDoc = (doc, depth, slug) => {
|
|
49
|
+
if (processed.has(doc.id))
|
|
50
|
+
return;
|
|
51
|
+
processed.add(doc.id);
|
|
52
|
+
const children = documents.filter((d) => d.parentId === doc.id);
|
|
53
|
+
const hasChildren = children.length > 0;
|
|
54
|
+
const treeSlug = `${slug}/${doc.slug}`;
|
|
55
|
+
items.push({ ...doc, depth, hasChildren, slug: treeSlug });
|
|
56
|
+
if (hasChildren && expandedFolders.has(doc.id)) {
|
|
57
|
+
children
|
|
58
|
+
.sort((a, b) => a.order - b.order)
|
|
59
|
+
.forEach((child) => processDoc(child, depth + 1, treeSlug));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
documents
|
|
63
|
+
.filter((d) => !d.parentId)
|
|
64
|
+
.sort((a, b) => a.order - b.order)
|
|
65
|
+
.forEach((doc) => processDoc(doc, 0, ''));
|
|
66
|
+
return items;
|
|
67
|
+
}, [documents, expandedFolders]);
|
|
68
|
+
const dropAnimation = null;
|
|
69
|
+
const handleDragStart = useCallback((event) => {
|
|
70
|
+
setActiveId(event.active.id);
|
|
71
|
+
}, []);
|
|
72
|
+
const getChildren = useCallback((parentId) => documents.filter((d) => d.parentId === parentId), [documents]);
|
|
73
|
+
const activeDoc = useMemo(() => {
|
|
74
|
+
if (!activeId)
|
|
75
|
+
return null;
|
|
76
|
+
const doc = documents.find((d) => d.id === activeId);
|
|
77
|
+
if (!doc)
|
|
78
|
+
return null;
|
|
79
|
+
const children = getChildren(doc.id);
|
|
80
|
+
return { ...doc, depth: 0, hasChildren: children.length > 0 };
|
|
81
|
+
}, [activeId, documents, getChildren]);
|
|
82
|
+
const handleDragOver = useCallback((event) => {
|
|
83
|
+
const { over, active } = event;
|
|
84
|
+
if (!over || active.id === over.id) {
|
|
85
|
+
setDropIndicator(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const activeIndex = flattenedTree.findIndex((item) => item.id === active.id);
|
|
89
|
+
const overIndex = flattenedTree.findIndex((item) => item.id === over.id);
|
|
90
|
+
if (activeIndex === -1 || overIndex === -1) {
|
|
91
|
+
setDropIndicator(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const position = activeIndex < overIndex ? 'after' : 'before';
|
|
95
|
+
setDropIndicator({ id: over.id, position });
|
|
96
|
+
}, [flattenedTree]);
|
|
97
|
+
const handleDragEnd = useCallback((event) => {
|
|
98
|
+
const { active, over } = event;
|
|
99
|
+
setActiveId(null);
|
|
100
|
+
setDropIndicator(null);
|
|
101
|
+
if (!over)
|
|
102
|
+
return;
|
|
103
|
+
const draggedId = active.id;
|
|
104
|
+
const overId = over.id;
|
|
105
|
+
if (draggedId === overId)
|
|
106
|
+
return;
|
|
107
|
+
const activeDoc = documents.find((d) => d.id === draggedId);
|
|
108
|
+
const overDoc = documents.find((d) => d.id === overId);
|
|
109
|
+
if (!activeDoc || !overDoc)
|
|
110
|
+
return;
|
|
111
|
+
let newParentId = null;
|
|
112
|
+
let newOrder = 0;
|
|
113
|
+
const activeIndex = flattenedTree.findIndex((item) => item.id === draggedId);
|
|
114
|
+
const overIndex = flattenedTree.findIndex((item) => item.id === overId);
|
|
115
|
+
if (activeIndex === -1 || overIndex === -1)
|
|
116
|
+
return;
|
|
117
|
+
const overSiblings = documents
|
|
118
|
+
.filter((d) => d.parentId === overDoc.parentId && d.id !== draggedId)
|
|
119
|
+
.sort((a, b) => a.order - b.order);
|
|
120
|
+
const overPosition = overSiblings.findIndex((s) => s.id === overId);
|
|
121
|
+
if (activeIndex < overIndex) {
|
|
122
|
+
newParentId = overDoc.parentId || null;
|
|
123
|
+
const nextSibling = overSiblings[overPosition + 1];
|
|
124
|
+
if (nextSibling) {
|
|
125
|
+
newOrder = (overDoc.order + nextSibling.order) / 2;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
newOrder = overDoc.order + 10;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
newParentId = overDoc.parentId || null;
|
|
133
|
+
const prevSibling = overSiblings[overPosition - 1];
|
|
134
|
+
if (prevSibling) {
|
|
135
|
+
newOrder = (prevSibling.order + overDoc.order) / 2;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
newOrder = overDoc.order - 10;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const overHasChildren = getChildren(overId).length > 0;
|
|
142
|
+
if (overHasChildren &&
|
|
143
|
+
activeIndex > overIndex &&
|
|
144
|
+
overDoc.parentId !== activeDoc.parentId) {
|
|
145
|
+
newParentId = overId;
|
|
146
|
+
newOrder = 0;
|
|
147
|
+
}
|
|
148
|
+
const isChildOfActive = (docId) => {
|
|
149
|
+
const doc = documents.find((d) => d.id === docId);
|
|
150
|
+
if (!doc)
|
|
151
|
+
return false;
|
|
152
|
+
if (doc.parentId === draggedId)
|
|
153
|
+
return true;
|
|
154
|
+
if (doc.parentId)
|
|
155
|
+
return isChildOfActive(doc.parentId);
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
if (isChildOfActive(overId))
|
|
159
|
+
return;
|
|
160
|
+
reorderMutation.mutate({
|
|
161
|
+
id: draggedId,
|
|
162
|
+
parentId: newParentId,
|
|
163
|
+
order: newOrder,
|
|
164
|
+
});
|
|
165
|
+
}, [documents, flattenedTree, getChildren, reorderMutation]);
|
|
166
|
+
const handleDragCancel = useCallback(() => {
|
|
167
|
+
setActiveId(null);
|
|
168
|
+
setDropIndicator(null);
|
|
169
|
+
}, []);
|
|
170
|
+
return (_jsxs("aside", { className: [
|
|
171
|
+
'border-r border-border flex flex-col transition-all duration-200 ease-in-out overflow-hidden',
|
|
172
|
+
isOpen ? 'w-64 opacity-100' : 'w-0 opacity-0',
|
|
173
|
+
].join(' '), children: [_jsx("div", { className: "p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "font-semibold text-sm text-muted-foreground", children: "Documents" }), _jsx("button", { className: "p-2 hover:bg-primary rounded-md transition-colors cursor-pointer", onClick: onCreate, children: _jsx(Plus, { className: "h-4 w-4" }) })] }) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2 space-y-1 relative", children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, onDragCancel: handleDragCancel, children: [flattenedTree
|
|
174
|
+
// .filter((doc) => doc.id !== activeId)
|
|
175
|
+
.map((doc) => (_jsx(SortableTreeItem, { id: doc.id, doc: doc, isExpanded: expandedFolders.has(doc.id), isCurrent: doc.id === resolvedCurrentDocId, dropIndicator: dropIndicator?.id === doc.id ? dropIndicator.position : null, onToggle: toggleFolder, onOpen: onOpen }, doc.id))), _jsx(DragOverlay, { dropAnimation: dropAnimation, children: activeDoc ? _jsx(DragOverlayItem, { doc: activeDoc }) : null })] }), documents.length === 0 && (_jsx("div", { className: "text-sm text-gray-500 text-center py-4", children: "No documents yet" }))] })] }));
|
|
176
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FlattenedItem } from '../../shared/types';
|
|
2
|
+
interface SortableTreeItemProps {
|
|
3
|
+
id: string;
|
|
4
|
+
doc: FlattenedItem;
|
|
5
|
+
isExpanded: boolean;
|
|
6
|
+
isCurrent: boolean;
|
|
7
|
+
dropIndicator: 'before' | 'after' | null;
|
|
8
|
+
onToggle: (id: string) => void;
|
|
9
|
+
onOpen: (path: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function SortableTreeItem({ id, doc, isExpanded, isCurrent, dropIndicator, onToggle, onOpen, }: SortableTreeItemProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=SortableTreeItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SortableTreeItem.d.ts","sourceRoot":"","sources":["../../../src/client/components/SortableTreeItem.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,aAAa,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAA;IACxC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,GAAG,EACH,UAAU,EACV,SAAS,EACT,aAAa,EACb,QAAQ,EACR,MAAM,GACP,EAAE,qBAAqB,2CA8EvB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
|
3
|
+
import { ChevronDownIcon, ChevronRightIcon, FileTextIcon, FolderIcon, FolderOpenIcon, } from 'lucide-react';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
export function SortableTreeItem({ id, doc, isExpanded, isCurrent, dropIndicator, onToggle, onOpen, }) {
|
|
6
|
+
const { attributes, listeners, setNodeRef: setDraggableRef, isDragging, } = useDraggable({ id });
|
|
7
|
+
const { setNodeRef: setDroppableRef } = useDroppable({ id });
|
|
8
|
+
const setNodeRef = useCallback((node) => {
|
|
9
|
+
setDraggableRef(node);
|
|
10
|
+
setDroppableRef(node);
|
|
11
|
+
}, [setDraggableRef, setDroppableRef]);
|
|
12
|
+
const style = {
|
|
13
|
+
opacity: isDragging ? 0.5 : 1,
|
|
14
|
+
paddingLeft: doc.depth * 12 + 8,
|
|
15
|
+
touchAction: 'none',
|
|
16
|
+
};
|
|
17
|
+
return (_jsxs("div", { className: "relative", children: [dropIndicator === 'before' && (_jsx("div", { className: "absolute top-0 left-2 right-2 h-0.5 bg-blue-500 rounded-full z-10" })), _jsxs("div", { ref: setNodeRef, style: style, ...attributes, ...listeners, onClick: () => onOpen(doc.slug), className: [
|
|
18
|
+
'flex items-center gap-1 w-full px-2 py-1.5 text-sm rounded-md transition-colors select-none cursor-pointer',
|
|
19
|
+
isCurrent ? 'font-bold bg-accent/50' : 'hover:bg-accent/30',
|
|
20
|
+
].join(' '), children: [doc.hasChildren ? (_jsx("button", { onClick: (e) => {
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
onToggle(doc.id);
|
|
23
|
+
}, className: "p-0.5 hover:bg-gray-200/50 dark:hover:bg-gray-700/50 rounded shrink-0 cursor-pointer", children: isExpanded ? (_jsx(ChevronDownIcon, { className: "h-4 w-4" })) : (_jsx(ChevronRightIcon, { className: "h-4 w-4" })) })) : (_jsx("div", { className: "w-5" })), _jsxs("div", { className: "flex items-center gap-1 flex-1 min-w-0", children: [doc.emoji ? (_jsx("span", { className: "text-base shrink-0", children: doc.emoji })) : doc.hasChildren ? (isExpanded ? (_jsx(FolderOpenIcon, { className: "h-4 w-4 shrink-0 text-muted-foreground" })) : (_jsx(FolderIcon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }))) : (_jsx(FileTextIcon, { className: "h-4 w-4 shrink-0 text-muted-foreground" })), _jsx("span", { className: "truncate flex-1", children: doc.title })] })] }), dropIndicator === 'after' && (_jsx("div", { className: "absolute bottom-0 left-2 right-2 h-0.5 bg-blue-500 rounded-full z-10" }))] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface VersionHistoryProps {
|
|
2
|
+
documentId: string;
|
|
3
|
+
documentTitle: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
onRestore?: () => void;
|
|
6
|
+
userName?: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Component for viewing and managing document version history
|
|
11
|
+
*/
|
|
12
|
+
export declare function VersionHistory({ documentId, documentTitle, baseUrl, onRestore, userName, userId, }: VersionHistoryProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export default VersionHistory;
|
|
14
|
+
//# sourceMappingURL=VersionHistory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VersionHistory.d.ts","sourceRoot":"","sources":["../../../src/client/components/VersionHistory.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAC7B,UAAU,EACV,aAAa,EACb,OAAgB,EAChB,SAAS,EACT,QAAsB,EACtB,MAAM,GACP,EAAE,mBAAmB,2CAsQrB;AAED,eAAe,cAAc,CAAA"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { History, RotateCcw, Trash2, GitCompare, Clock, User, MessageSquare, } from 'lucide-react';
|
|
4
|
+
import { useDocumentVersionsQuery, useCreateVersionMutation, useRestoreVersionMutation, useDeleteVersionMutation, } from '../hooks/useVersions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Component for viewing and managing document version history
|
|
7
|
+
*/
|
|
8
|
+
export function VersionHistory({ documentId, documentTitle, baseUrl = '/api', onRestore, userName = 'Anonymous', userId, }) {
|
|
9
|
+
const { data, isLoading, error } = useDocumentVersionsQuery(documentId, baseUrl);
|
|
10
|
+
const createVersionMutation = useCreateVersionMutation(baseUrl);
|
|
11
|
+
const restoreMutation = useRestoreVersionMutation(baseUrl);
|
|
12
|
+
const deleteMutation = useDeleteVersionMutation(baseUrl);
|
|
13
|
+
const [selectedVersions, setSelectedVersions] = useState([]);
|
|
14
|
+
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
15
|
+
const [versionDescription, setVersionDescription] = useState('');
|
|
16
|
+
const [restoringVersion, setRestoringVersion] = useState(null);
|
|
17
|
+
const versions = data?.items || [];
|
|
18
|
+
const handleCreateVersion = async () => {
|
|
19
|
+
try {
|
|
20
|
+
await createVersionMutation.mutateAsync({
|
|
21
|
+
documentId,
|
|
22
|
+
data: {
|
|
23
|
+
description: versionDescription || `Version created by ${userName}`,
|
|
24
|
+
createdBy: userId,
|
|
25
|
+
createdByName: userName,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
setShowCreateDialog(false);
|
|
29
|
+
setVersionDescription('');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
alert('Failed to create version');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const handleRestore = async (version) => {
|
|
36
|
+
if (!confirm(`Restore "${documentTitle}" to version ${version.versionNumber}?\n\nCurrent state will be saved as a backup.`)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setRestoringVersion(version.id);
|
|
40
|
+
try {
|
|
41
|
+
await restoreMutation.mutateAsync({
|
|
42
|
+
documentId,
|
|
43
|
+
versionId: version.id,
|
|
44
|
+
data: {
|
|
45
|
+
restoredBy: userId,
|
|
46
|
+
restoredByName: userName,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
onRestore?.();
|
|
50
|
+
alert(`Restored to version ${version.versionNumber}`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
alert('Failed to restore version');
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setRestoringVersion(null);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const handleDelete = async (version) => {
|
|
60
|
+
if (!confirm(`Delete version ${version.versionNumber}? This cannot be undone.`)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await deleteMutation.mutateAsync({ versionId: version.id, documentId });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
alert('Failed to delete version');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const toggleVersionSelection = (versionId) => {
|
|
71
|
+
setSelectedVersions((prev) => {
|
|
72
|
+
if (prev.includes(versionId)) {
|
|
73
|
+
return prev.filter((id) => id !== versionId);
|
|
74
|
+
}
|
|
75
|
+
if (prev.length >= 2) {
|
|
76
|
+
return [prev[1], versionId];
|
|
77
|
+
}
|
|
78
|
+
return [...prev, versionId];
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const formatDate = (dateString) => {
|
|
82
|
+
const date = new Date(dateString);
|
|
83
|
+
return date.toLocaleString(undefined, {
|
|
84
|
+
month: 'short',
|
|
85
|
+
day: 'numeric',
|
|
86
|
+
year: 'numeric',
|
|
87
|
+
hour: '2-digit',
|
|
88
|
+
minute: '2-digit',
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
if (isLoading) {
|
|
92
|
+
return (_jsxs("div", { className: "p-4 text-center text-gray-500", children: [_jsx("div", { className: "animate-spin inline-block w-5 h-5 border-2 border-gray-300 border-t-gray-600 rounded-full mr-2" }), "Loading versions..."] }));
|
|
93
|
+
}
|
|
94
|
+
if (error) {
|
|
95
|
+
return (_jsx("div", { className: "p-4 text-center text-red-500", children: "Failed to load version history" }));
|
|
96
|
+
}
|
|
97
|
+
return (_jsxs("div", { className: "version-history bg-white rounded-lg border border-gray-200", children: [_jsxs("div", { className: "flex items-center justify-between p-4 border-b border-gray-200", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(History, { className: "w-5 h-5 text-gray-500" }), _jsx("h3", { className: "font-semibold text-gray-900", children: "Version History" }), _jsxs("span", { className: "text-sm text-gray-500", children: ["(", versions.length, ")"] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [selectedVersions.length === 2 && (_jsxs("button", { className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100", onClick: () => { }, children: [_jsx(GitCompare, { className: "w-4 h-4" }), "Compare"] })), _jsx("button", { className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-gray-900 text-white rounded-md hover:bg-gray-800", onClick: () => setShowCreateDialog(true), children: "Save Version" })] })] }), showCreateDialog && (_jsx("div", { className: "p-4 bg-gray-50 border-b border-gray-200", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(MessageSquare, { className: "w-5 h-5 text-gray-400 mt-2" }), _jsxs("div", { className: "flex-1", children: [_jsx("input", { type: "text", placeholder: "Describe what's changed in this version...", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", value: versionDescription, onChange: (e) => setVersionDescription(e.target.value), onKeyDown: (e) => e.key === 'Enter' && handleCreateVersion() }), _jsxs("div", { className: "flex items-center gap-2 mt-2", children: [_jsx("button", { className: "px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50", onClick: handleCreateVersion, disabled: createVersionMutation.isPending, children: createVersionMutation.isPending ? 'Saving...' : 'Save' }), _jsx("button", { className: "px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900", onClick: () => {
|
|
98
|
+
setShowCreateDialog(false);
|
|
99
|
+
setVersionDescription('');
|
|
100
|
+
}, children: "Cancel" })] })] })] }) })), _jsx("div", { className: "max-h-96 overflow-y-auto", children: versions.length === 0 ? (_jsxs("div", { className: "p-8 text-center text-gray-500", children: [_jsx(History, { className: "w-12 h-12 mx-auto mb-3 text-gray-300" }), _jsx("p", { children: "No versions yet" }), _jsx("p", { className: "text-sm mt-1", children: "Save a version to track changes" })] })) : (_jsx("div", { className: "divide-y divide-gray-100", children: versions.map((version) => (_jsx("div", { className: `p-4 hover:bg-gray-50 transition-colors ${selectedVersions.includes(version.id) ? 'bg-blue-50' : ''}`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("input", { type: "checkbox", checked: selectedVersions.includes(version.id), onChange: () => toggleVersionSelection(version.id), className: "mt-1 w-4 h-4 text-blue-600 rounded border-gray-300" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "font-mono text-sm font-medium text-gray-900", children: ["v", version.versionNumber] }), version.versionNumber === versions[0]?.versionNumber && (_jsx("span", { className: "px-2 py-0.5 text-xs bg-blue-100 text-blue-700 rounded-full", children: "Latest" }))] }), version.changeDescription && (_jsx("p", { className: "text-sm text-gray-700 mt-1", children: version.changeDescription })), _jsxs("div", { className: "flex items-center gap-4 mt-2 text-xs text-gray-500", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Clock, { className: "w-3 h-3" }), formatDate(version.createdAt)] }), version.createdByName && (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(User, { className: "w-3 h-3" }), version.createdByName] }))] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { className: "p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded", onClick: () => handleRestore(version), disabled: restoringVersion === version.id, title: "Restore this version", children: restoringVersion === version.id ? (_jsx("div", { className: "w-4 h-4 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin" })) : (_jsx(RotateCcw, { className: "w-4 h-4" })) }), _jsx("button", { className: "p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded", onClick: () => handleDelete(version), disabled: deleteMutation.isPending, title: "Delete version", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }) }, version.id))) })) })] }));
|
|
101
|
+
}
|
|
102
|
+
export default VersionHistory;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { BlockNoteEditor } from '@blocknote/core';
|
|
2
|
+
/**
|
|
3
|
+
* User info for collaborative editing
|
|
4
|
+
*/
|
|
5
|
+
export interface CollaboratorInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
color: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Awareness state from other users
|
|
12
|
+
*/
|
|
13
|
+
export interface CollaboratorState {
|
|
14
|
+
user: CollaboratorInfo;
|
|
15
|
+
cursor: {
|
|
16
|
+
blockId: string;
|
|
17
|
+
index: number;
|
|
18
|
+
} | null;
|
|
19
|
+
selection: {
|
|
20
|
+
anchor: {
|
|
21
|
+
blockId: string;
|
|
22
|
+
index: number;
|
|
23
|
+
};
|
|
24
|
+
head: {
|
|
25
|
+
blockId: string;
|
|
26
|
+
index: number;
|
|
27
|
+
};
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Options for useCollaborativeEditor
|
|
32
|
+
*/
|
|
33
|
+
export interface UseCollaborativeEditorOptions {
|
|
34
|
+
documentId: string;
|
|
35
|
+
userId: string;
|
|
36
|
+
userName: string;
|
|
37
|
+
serverUrl?: string;
|
|
38
|
+
initialContent?: any[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Return type for useCollaborativeEditor
|
|
42
|
+
*/
|
|
43
|
+
export interface UseCollaborativeEditorReturn {
|
|
44
|
+
editor: BlockNoteEditor | null;
|
|
45
|
+
isConnected: boolean;
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
collaborators: CollaboratorState[];
|
|
48
|
+
error: Error | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Hook for real-time collaborative editing with Yjs
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* function CollaborativeEditor({ docId }: { docId: string }) {
|
|
56
|
+
* const { editor, isConnected, collaborators } = useCollaborativeEditor({
|
|
57
|
+
* documentId: docId,
|
|
58
|
+
* userId: 'user-123',
|
|
59
|
+
* userName: 'John Doe',
|
|
60
|
+
* })
|
|
61
|
+
*
|
|
62
|
+
* if (!editor) return <div>Loading...</div>
|
|
63
|
+
*
|
|
64
|
+
* return (
|
|
65
|
+
* <div>
|
|
66
|
+
* <div className="collaborators">
|
|
67
|
+
* {collaborators.map(c => (
|
|
68
|
+
* <span key={c.user.id} style={{ color: c.user.color }}>
|
|
69
|
+
* {c.user.name}
|
|
70
|
+
* </span>
|
|
71
|
+
* ))}
|
|
72
|
+
* </div>
|
|
73
|
+
* <BlockNoteView editor={editor} />
|
|
74
|
+
* </div>
|
|
75
|
+
* )
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function useCollaborativeEditor(options: UseCollaborativeEditorOptions): UseCollaborativeEditorReturn;
|
|
80
|
+
/**
|
|
81
|
+
* Hook for non-collaborative editing (fallback)
|
|
82
|
+
*/
|
|
83
|
+
export declare function useEditor(content?: any[]): {
|
|
84
|
+
editor: BlockNoteEditor | null;
|
|
85
|
+
isLoading: boolean;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Hook to fetch active collaborators for a document (read-only)
|
|
89
|
+
*/
|
|
90
|
+
export declare function useCollaborators(documentId: string, baseUrl?: string): {
|
|
91
|
+
collaborators: {
|
|
92
|
+
userId: string;
|
|
93
|
+
userName: string | null;
|
|
94
|
+
userColor: string | null;
|
|
95
|
+
lastSeenAt: Date;
|
|
96
|
+
}[];
|
|
97
|
+
isLoading: boolean;
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=useCollaboration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCollaboration.d.ts","sourceRoot":"","sources":["../../../src/client/hooks/useCollaboration.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACjD,SAAS,EAAE;QAAE,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAA;CAC3G;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,GAAG,EAAE,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,iBAAiB,EAAE,CAAA;IAClC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,4BAA4B,CAmG9B;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,OAAO,CAAC,EAAE,GAAG,EAAE,GACd;IAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAcxD;AAyBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,MAAe;;gBAEjE,MAAM;kBACJ,MAAM,GAAG,IAAI;mBACZ,MAAM,GAAG,IAAI;oBACZ,IAAI;;;EA0BnB"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
4
|
+
import { useCreateBlockNote } from '@blocknote/react';
|
|
5
|
+
/**
|
|
6
|
+
* Hook for real-time collaborative editing with Yjs
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function CollaborativeEditor({ docId }: { docId: string }) {
|
|
11
|
+
* const { editor, isConnected, collaborators } = useCollaborativeEditor({
|
|
12
|
+
* documentId: docId,
|
|
13
|
+
* userId: 'user-123',
|
|
14
|
+
* userName: 'John Doe',
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* if (!editor) return <div>Loading...</div>
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <div className="collaborators">
|
|
22
|
+
* {collaborators.map(c => (
|
|
23
|
+
* <span key={c.user.id} style={{ color: c.user.color }}>
|
|
24
|
+
* {c.user.name}
|
|
25
|
+
* </span>
|
|
26
|
+
* ))}
|
|
27
|
+
* </div>
|
|
28
|
+
* <BlockNoteView editor={editor} />
|
|
29
|
+
* </div>
|
|
30
|
+
* )
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useCollaborativeEditor(options) {
|
|
35
|
+
const { documentId, userId, userName, serverUrl = window.location.origin, initialContent } = options;
|
|
36
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
38
|
+
const [collaborators, setCollaborators] = useState([]);
|
|
39
|
+
const [error, setError] = useState(null);
|
|
40
|
+
// Generate user color based on userId
|
|
41
|
+
const userColor = useMemo(() => generateColor(userId), [userId]);
|
|
42
|
+
// Create Yjs document (singleton per documentId)
|
|
43
|
+
const ydoc = useMemo(() => {
|
|
44
|
+
return new Y.Doc();
|
|
45
|
+
}, [documentId]);
|
|
46
|
+
// Create WebSocket provider
|
|
47
|
+
const provider = useMemo(() => {
|
|
48
|
+
const wsUrl = serverUrl.replace(/^http/, 'ws');
|
|
49
|
+
const provider = new WebsocketProvider(`${wsUrl}/api/collab`, documentId, ydoc);
|
|
50
|
+
return provider;
|
|
51
|
+
}, [ydoc, documentId, serverUrl]);
|
|
52
|
+
// Create BlockNote editor with collaboration
|
|
53
|
+
const editor = useCreateBlockNote({
|
|
54
|
+
collaboration: {
|
|
55
|
+
provider,
|
|
56
|
+
fragment: ydoc.getXmlFragment('document-store'),
|
|
57
|
+
user: {
|
|
58
|
+
name: userName,
|
|
59
|
+
color: userColor,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Set up connection status and awareness
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
// Connection status
|
|
68
|
+
provider.on('status', (event) => {
|
|
69
|
+
setIsConnected(event.status === 'connected');
|
|
70
|
+
if (event.status === 'connected') {
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Set user awareness
|
|
75
|
+
provider.awareness.setLocalStateField('user', {
|
|
76
|
+
id: userId,
|
|
77
|
+
name: userName,
|
|
78
|
+
color: userColor,
|
|
79
|
+
});
|
|
80
|
+
// Listen for other users
|
|
81
|
+
const handleAwarenessChange = () => {
|
|
82
|
+
const states = Array.from(provider.awareness.getStates().entries());
|
|
83
|
+
const others = states
|
|
84
|
+
.filter(([clientId]) => clientId !== provider.awareness.clientID)
|
|
85
|
+
.map(([, state]) => state)
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
setCollaborators(others);
|
|
88
|
+
};
|
|
89
|
+
provider.awareness.on('change', handleAwarenessChange);
|
|
90
|
+
handleAwarenessChange(); // Initial call
|
|
91
|
+
// Error handling
|
|
92
|
+
provider.on('connection-error', (event) => {
|
|
93
|
+
setError(new Error('Connection error'));
|
|
94
|
+
setIsLoading(false);
|
|
95
|
+
});
|
|
96
|
+
return () => {
|
|
97
|
+
provider.awareness.off('change', handleAwarenessChange);
|
|
98
|
+
};
|
|
99
|
+
}, [provider, userId, userName, userColor]);
|
|
100
|
+
// Cleanup on unmount
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
provider.destroy();
|
|
104
|
+
ydoc.destroy();
|
|
105
|
+
};
|
|
106
|
+
}, [provider, ydoc]);
|
|
107
|
+
return {
|
|
108
|
+
editor,
|
|
109
|
+
isConnected,
|
|
110
|
+
isLoading,
|
|
111
|
+
collaborators,
|
|
112
|
+
error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hook for non-collaborative editing (fallback)
|
|
117
|
+
*/
|
|
118
|
+
export function useEditor(content) {
|
|
119
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
120
|
+
const editor = useCreateBlockNote({
|
|
121
|
+
initialContent: content || [{ type: 'paragraph', content: '' }],
|
|
122
|
+
});
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (editor) {
|
|
125
|
+
setIsLoading(false);
|
|
126
|
+
}
|
|
127
|
+
}, [editor]);
|
|
128
|
+
return { editor, isLoading };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Generate a consistent color for a user
|
|
132
|
+
*/
|
|
133
|
+
function generateColor(userId) {
|
|
134
|
+
const colors = [
|
|
135
|
+
'#EF4444', // red
|
|
136
|
+
'#F97316', // orange
|
|
137
|
+
'#F59E0B', // amber
|
|
138
|
+
'#84CC16', // lime
|
|
139
|
+
'#10B981', // emerald
|
|
140
|
+
'#06B6D4', // cyan
|
|
141
|
+
'#3B82F6', // blue
|
|
142
|
+
'#8B5CF6', // violet
|
|
143
|
+
'#EC4899', // pink
|
|
144
|
+
'#F43F5E', // rose
|
|
145
|
+
];
|
|
146
|
+
let hash = 0;
|
|
147
|
+
for (let i = 0; i < userId.length; i++) {
|
|
148
|
+
hash = userId.charCodeAt(i) + ((hash << 5) - hash);
|
|
149
|
+
}
|
|
150
|
+
return colors[Math.abs(hash) % colors.length];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Hook to fetch active collaborators for a document (read-only)
|
|
154
|
+
*/
|
|
155
|
+
export function useCollaborators(documentId, baseUrl = '/api') {
|
|
156
|
+
const [collaborators, setCollaborators] = useState([]);
|
|
157
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const fetchCollaborators = async () => {
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${baseUrl}/docs/${documentId}/collaborators`);
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
throw new Error('Failed to fetch collaborators');
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
setCollaborators(data.items);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('Failed to fetch collaborators:', error);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
setIsLoading(false);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
fetchCollaborators();
|
|
175
|
+
// Poll every 30 seconds
|
|
176
|
+
const interval = setInterval(fetchCollaborators, 30000);
|
|
177
|
+
return () => clearInterval(interval);
|
|
178
|
+
}, [documentId, baseUrl]);
|
|
179
|
+
return { collaborators, isLoading };
|
|
180
|
+
}
|