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