1
+ import { createClient } from "actor-core/client" ;
2
+ import { createReactActorCore } from "@actor-core/react" ;
3
+ import { useState , useEffect } from "react" ;
4
+ import type { App , Cursor , CursorUpdateEvent , TextUpdatedEvent , UserDisconnectedEvent } from "../actors/app" ;
5
+
6
+ const client = createClient < App > ( "http://localhost:6420" ) ;
7
+ const { useActor, useActorEvent } = createReactActorCore ( client ) ;
8
+
9
+ function DocumentEditor ( ) {
10
+ // Connect to actor for this document ID from URL
11
+ const documentId = new URLSearchParams ( window . location . search ) . get ( 'id' ) || 'default-doc' ;
12
+ const [ { actor, state } ] = useActor ( "document" , { tags : { id : documentId } } ) ;
13
+
14
+ // Local state
15
+ const [ text , setText ] = useState ( "" ) ;
16
+ const [ cursorPos , setCursorPos ] = useState ( { x : 0 , y : 0 } ) ;
17
+ const [ otherCursors , setOtherCursors ] = useState < Record < string , Cursor > > ( { } ) ;
18
+
19
+ // Load initial document state
20
+ useEffect ( ( ) => {
21
+ if ( actor && state === "created" ) {
22
+ actor . getText ( ) . then ( setText ) ;
23
+ actor . getCursors ( ) . then ( setOtherCursors ) ;
24
+ }
25
+ } , [ actor , state ] ) ;
26
+
27
+ // Listen for updates from other users
28
+ useActorEvent ( { actor, event : "textUpdated" } , ( event ) => {
29
+ const { text : newText , userId : _senderId } = event as TextUpdatedEvent ;
30
+
31
+ setText ( newText ) ;
32
+ } ) ;
33
+
34
+ useActorEvent ( { actor, event : "cursorUpdated" } , ( event ) => {
35
+ const { userId : cursorUserId , x, y } = event as CursorUpdateEvent ;
36
+
37
+ setOtherCursors ( prev => ( {
38
+ ...prev ,
39
+ [ cursorUserId ] : { x, y, userId : cursorUserId }
40
+ } ) ) ;
41
+ } ) ;
42
+
43
+ useActorEvent ( { actor, event : "userDisconnected" } , ( event ) => {
44
+ const { userId } = event as UserDisconnectedEvent ;
45
+
46
+ setOtherCursors ( prev => {
47
+ const newCursors = { ...prev } ;
48
+ delete newCursors [ userId ] ;
49
+ return newCursors ;
50
+ } ) ;
51
+ } ) ;
52
+
53
+
54
+ useEffect ( ( ) => {
55
+ if ( ! actor || state !== "created" ) return ;
56
+
57
+ const updateCursor = ( { x, y } : { x : number , y : number } ) => {
58
+
59
+ if ( x !== cursorPos . x || y !== cursorPos . y ) {
60
+ setCursorPos ( { x, y } ) ;
61
+ actor . updateCursor ( x , y ) ;
62
+ }
63
+ } ;
64
+
65
+ window . addEventListener ( "mousemove" , ( e ) => {
66
+ const x = e . clientX ;
67
+ const y = e . clientY ;
68
+
69
+ updateCursor ( { x, y } ) ;
70
+ } ) ;
71
+ } , [ actor , state ] ) ;
72
+
73
+ return (
74
+ < div className = "document-editor" >
75
+ < h2 > Document: { documentId } </ h2 >
76
+
77
+ < div >
78
+ < textarea
79
+ value = { text }
80
+ onChange = { ( e ) => {
81
+ const newText = e . target . value ;
82
+ setText ( newText ) ;
83
+ if ( actor && state === "created" ) {
84
+ actor . setText ( newText ) ;
85
+ }
86
+ } }
87
+ placeholder = "Start typing..."
88
+ />
89
+
90
+ { /* Other users' cursors */ }
91
+ { Object . values ( otherCursors ) . map ( ( cursor ) => (
92
+ < div
93
+ key = { cursor . userId }
94
+ style = { {
95
+ position : 'absolute' ,
96
+ left : `${ cursor . x } px` ,
97
+ top : `${ cursor . y } px` ,
98
+ width : '10px' ,
99
+ height : '10px' ,
100
+ backgroundColor : 'red' ,
101
+ borderRadius : '50%'
102
+ } }
103
+ />
104
+ ) ) }
105
+ </ div >
106
+
107
+ < div >
108
+ < p > Connected users: You and { Object . keys ( otherCursors ) . length } others</ p >
109
+ </ div >
110
+ </ div >
111
+ ) ;
112
+ }
113
+
114
+ export default function ReactApp ( ) {
115
+ return (
116
+ < div className = "app" >
117
+ < h1 > Collaborative Document Editor</ h1 >
118
+ < DocumentEditor />
119
+ </ div >
120
+ ) ;
121
+ }
0 commit comments