pyjs-examples 0.0.1__py3-none-any.whl
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.
- pyjs_examples/__init__.py +1 -0
- pyjs_examples/calculator/README.md +55 -0
- pyjs_examples/calculator/__init__.py +1 -0
- pyjs_examples/calculator/__main__.py +9 -0
- pyjs_examples/calculator/calculator.py +325 -0
- pyjs_examples/calculator/tests/test_browser_calculator.py +67 -0
- pyjs_examples/calculator/tests/test_calculator.py +72 -0
- pyjs_examples/catalog.py +1433 -0
- pyjs_examples/chatroom/README.md +50 -0
- pyjs_examples/chatroom/__init__.py +1 -0
- pyjs_examples/chatroom/__main__.py +12 -0
- pyjs_examples/chatroom/chat_server.py +156 -0
- pyjs_examples/chatroom/chatroom.py +704 -0
- pyjs_examples/chatroom/tests/test_browser_chatroom.py +59 -0
- pyjs_examples/chatroom/tests/test_chat_server.py +53 -0
- pyjs_examples/chatroom/visual_test.py +156 -0
- pyjs_examples/counter.py +73 -0
- pyjs_examples/kanban/README.md +31 -0
- pyjs_examples/kanban/__init__.py +1 -0
- pyjs_examples/kanban/__main__.py +8 -0
- pyjs_examples/kanban/kanban.py +581 -0
- pyjs_examples/kanban/tests/renders/kanban-board.png +0 -0
- pyjs_examples/kanban/tests/test_browser_kanban.py +150 -0
- pyjs_examples/kanban/tests/test_kanban.py +38 -0
- pyjs_examples/kanban/visual_test.py +108 -0
- pyjs_examples/nativescript_counter/README.md +31 -0
- pyjs_examples/nativescript_counter/__init__.py +5 -0
- pyjs_examples/nativescript_counter/__main__.py +17 -0
- pyjs_examples/nativescript_counter/counter.py +54 -0
- pyjs_examples/nativescript_counter/tests/test_android_counter.py +31 -0
- pyjs_examples/nativescript_counter/tests/test_counter.py +11 -0
- pyjs_examples/samples/__init__.py +1 -0
- pyjs_examples/samples/state_counter/README.md +19 -0
- pyjs_examples/samples/state_counter/__init__.py +1 -0
- pyjs_examples/samples/state_counter/__main__.py +3 -0
- pyjs_examples/samples/state_counter/state_counter.py +60 -0
- pyjs_examples/samples/state_counter/tests/test_browser_state_counter.py +18 -0
- pyjs_examples/samples/state_counter/tests/test_state_counter.py +17 -0
- pyjs_examples/samples/ui_dialog/README.md +19 -0
- pyjs_examples/samples/ui_dialog/__init__.py +1 -0
- pyjs_examples/samples/ui_dialog/__main__.py +3 -0
- pyjs_examples/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
- pyjs_examples/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
- pyjs_examples/samples/ui_dialog/ui_dialog.py +65 -0
- pyjs_examples/space_invaders/README.md +45 -0
- pyjs_examples/space_invaders/__init__.py +1 -0
- pyjs_examples/space_invaders/__main__.py +3 -0
- pyjs_examples/space_invaders/space_invaders.py +1025 -0
- pyjs_examples/space_invaders/tests/renders/action_scene.png +0 -0
- pyjs_examples/space_invaders/tests/renders/initial_wave.png +0 -0
- pyjs_examples/space_invaders/tests/test_browser_game.py +86 -0
- pyjs_examples/space_invaders/tests/test_game.py +83 -0
- pyjs_examples/todomvc/README.md +50 -0
- pyjs_examples/todomvc/__init__.py +1 -0
- pyjs_examples/todomvc/__main__.py +12 -0
- pyjs_examples/todomvc/base.css +141 -0
- pyjs_examples/todomvc/index.css +393 -0
- pyjs_examples/todomvc/tests/test_browser_todomvc.py +145 -0
- pyjs_examples/todomvc/tests/test_todomvc.py +84 -0
- pyjs_examples/todomvc/todomvc.py +426 -0
- pyjs_examples/workspace/README.md +25 -0
- pyjs_examples/workspace/__init__.py +1 -0
- pyjs_examples/workspace/__main__.py +3 -0
- pyjs_examples/workspace/tests/test_browser_workspace.py +109 -0
- pyjs_examples/workspace/tests/test_workspace.py +27 -0
- pyjs_examples/workspace/workspace.py +749 -0
- pyjs_examples-0.0.1.dist-info/METADATA +71 -0
- pyjs_examples-0.0.1.dist-info/RECORD +71 -0
- pyjs_examples-0.0.1.dist-info/WHEEL +5 -0
- pyjs_examples-0.0.1.dist-info/entry_points.txt +11 -0
- pyjs_examples-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
from pyjs import effect, js, nojs, state
|
|
2
|
+
from pyjs.web.dom import location, sessionStorage
|
|
3
|
+
from pyjs.web.domx import button, form, input, ref, tag, tw
|
|
4
|
+
from pyjs.web.events import Event, MessageEvent
|
|
5
|
+
from pyjs.web.javascript import JSON
|
|
6
|
+
from pyjs.web.network import WebSocket
|
|
7
|
+
from pyjs.web.ui import Widget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@js
|
|
11
|
+
class ChatMessage:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.id = 0
|
|
14
|
+
self.room = ""
|
|
15
|
+
self.sender = ""
|
|
16
|
+
self.text = ""
|
|
17
|
+
self.time = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@js
|
|
21
|
+
class ChatSnapshot:
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.type = ""
|
|
24
|
+
self.room = ""
|
|
25
|
+
self.rooms: list[str] = []
|
|
26
|
+
self.messages: list[ChatMessage] = []
|
|
27
|
+
self.users: list[str] = []
|
|
28
|
+
self.message = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@js
|
|
32
|
+
class OutgoingChatMessage:
|
|
33
|
+
def __init__(self, message_type: str, name: str, room: str, text: str):
|
|
34
|
+
self.type = message_type
|
|
35
|
+
self.name = name
|
|
36
|
+
self.room = room
|
|
37
|
+
self.text = text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@js
|
|
41
|
+
class NameSubmitListener:
|
|
42
|
+
def __init__(self, app: ChatRoomApp):
|
|
43
|
+
self.app = app
|
|
44
|
+
|
|
45
|
+
@js(include=True)
|
|
46
|
+
def handleEvent(self, event: Event):
|
|
47
|
+
event.preventDefault()
|
|
48
|
+
self.app.submit_name()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@js
|
|
52
|
+
class RoomCreateListener:
|
|
53
|
+
def __init__(self, app: ChatRoomApp):
|
|
54
|
+
self.app = app
|
|
55
|
+
|
|
56
|
+
@js(include=True)
|
|
57
|
+
def handleEvent(self, event: Event):
|
|
58
|
+
event.preventDefault()
|
|
59
|
+
self.app.create_room()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@js
|
|
63
|
+
class MessageSubmitListener:
|
|
64
|
+
def __init__(self, app: ChatRoomApp):
|
|
65
|
+
self.app = app
|
|
66
|
+
|
|
67
|
+
@js(include=True)
|
|
68
|
+
def handleEvent(self, event: Event):
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
self.app.submit_message()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@js
|
|
74
|
+
class RoomClickListener:
|
|
75
|
+
def __init__(self, app: ChatRoomApp, room: str):
|
|
76
|
+
self.app = app
|
|
77
|
+
self.room = room
|
|
78
|
+
|
|
79
|
+
@js(include=True)
|
|
80
|
+
def handleEvent(self, event: Event):
|
|
81
|
+
self.app.switch_room(self.room)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@js
|
|
85
|
+
class ReconnectClickListener:
|
|
86
|
+
def __init__(self, app: ChatRoomApp):
|
|
87
|
+
self.app = app
|
|
88
|
+
|
|
89
|
+
@js(include=True)
|
|
90
|
+
def handleEvent(self, event: Event):
|
|
91
|
+
self.app.connect()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@js
|
|
95
|
+
class SignOutClickListener:
|
|
96
|
+
def __init__(self, app: ChatRoomApp):
|
|
97
|
+
self.app = app
|
|
98
|
+
|
|
99
|
+
@js(include=True)
|
|
100
|
+
def handleEvent(self, event: Event):
|
|
101
|
+
self.app.sign_out()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@js
|
|
105
|
+
class SocketOpenListener:
|
|
106
|
+
def __init__(self, app: ChatRoomApp):
|
|
107
|
+
self.app = app
|
|
108
|
+
|
|
109
|
+
@js(include=True)
|
|
110
|
+
def handleEvent(self, event: Event):
|
|
111
|
+
self.app.socket_opened()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@js
|
|
115
|
+
class SocketMessageListener:
|
|
116
|
+
def __init__(self, app: ChatRoomApp):
|
|
117
|
+
self.app = app
|
|
118
|
+
|
|
119
|
+
@js(include=True)
|
|
120
|
+
def handleEvent(self, event: MessageEvent):
|
|
121
|
+
message: ChatSnapshot = JSON.parse(event.data)
|
|
122
|
+
self.app.receive(message)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@js
|
|
126
|
+
class SocketCloseListener:
|
|
127
|
+
def __init__(self, app: ChatRoomApp):
|
|
128
|
+
self.app = app
|
|
129
|
+
|
|
130
|
+
@js(include=True)
|
|
131
|
+
def handleEvent(self, event: Event):
|
|
132
|
+
self.app.socket_closed()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@js
|
|
136
|
+
class SocketErrorListener:
|
|
137
|
+
def __init__(self, app: ChatRoomApp):
|
|
138
|
+
self.app = app
|
|
139
|
+
|
|
140
|
+
@js(include=True)
|
|
141
|
+
def handleEvent(self, event: Event):
|
|
142
|
+
self.app.show_error("The chat server could not be reached.")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@js
|
|
146
|
+
class ChatRoomApp(Widget):
|
|
147
|
+
name = state("")
|
|
148
|
+
room = state("general")
|
|
149
|
+
connection = state("signed-out")
|
|
150
|
+
socket_path = state("")
|
|
151
|
+
|
|
152
|
+
def __init__(self, socket_path: str = ""):
|
|
153
|
+
super().__init__()
|
|
154
|
+
self.socket_path = socket_path
|
|
155
|
+
|
|
156
|
+
self.name_input = ref(
|
|
157
|
+
input(
|
|
158
|
+
tw(
|
|
159
|
+
"w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-base "
|
|
160
|
+
"text-slate-950 outline-none placeholder:text-slate-400 "
|
|
161
|
+
"focus:border-violet-500 focus:ring-4 focus:ring-violet-100"
|
|
162
|
+
),
|
|
163
|
+
{
|
|
164
|
+
"autocomplete": "nickname",
|
|
165
|
+
"data-testid": "name-input",
|
|
166
|
+
"maxlength": "32",
|
|
167
|
+
"placeholder": "How should people know you?",
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
self.name_form = ref(
|
|
172
|
+
form(
|
|
173
|
+
tw("mt-6 space-y-3"),
|
|
174
|
+
self.name_input,
|
|
175
|
+
button(
|
|
176
|
+
tw(
|
|
177
|
+
"w-full rounded-xl bg-violet-600 px-4 py-3 font-semibold text-white "
|
|
178
|
+
"shadow-sm hover:bg-violet-700 focus:outline-none focus:ring-4 "
|
|
179
|
+
"focus:ring-violet-200"
|
|
180
|
+
),
|
|
181
|
+
{"type": "submit"},
|
|
182
|
+
"Enter workspace",
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
self.name_form.addEventListener("submit", NameSubmitListener(self))
|
|
187
|
+
self.sign_in = ref(
|
|
188
|
+
tag(
|
|
189
|
+
"section",
|
|
190
|
+
tw(
|
|
191
|
+
"fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-6 "
|
|
192
|
+
"backdrop-blur-sm"
|
|
193
|
+
),
|
|
194
|
+
{"data-testid": "sign-in"},
|
|
195
|
+
tag(
|
|
196
|
+
"div",
|
|
197
|
+
tw("w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl"),
|
|
198
|
+
tag(
|
|
199
|
+
"div",
|
|
200
|
+
tw(
|
|
201
|
+
"mb-5 flex h-12 w-12 items-center justify-center rounded-2xl "
|
|
202
|
+
"bg-violet-600 text-xl font-bold text-white"
|
|
203
|
+
),
|
|
204
|
+
"P",
|
|
205
|
+
),
|
|
206
|
+
tag(
|
|
207
|
+
"h1",
|
|
208
|
+
tw("text-2xl font-bold tracking-tight text-slate-950"),
|
|
209
|
+
"Join PyJS Workspace",
|
|
210
|
+
),
|
|
211
|
+
tag(
|
|
212
|
+
"p",
|
|
213
|
+
tw("mt-2 text-sm leading-6 text-slate-500"),
|
|
214
|
+
"Choose a display name for this tab. Nothing is saved when the "
|
|
215
|
+
"server stops.",
|
|
216
|
+
),
|
|
217
|
+
self.name_form,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self.room_input = ref(
|
|
223
|
+
input(
|
|
224
|
+
tw(
|
|
225
|
+
"min-w-0 flex-1 rounded-lg border border-white/15 bg-white/10 px-3 py-2 "
|
|
226
|
+
"text-sm text-white outline-none placeholder:text-violet-200/70 "
|
|
227
|
+
"focus:border-white/40 focus:bg-white/15"
|
|
228
|
+
),
|
|
229
|
+
{
|
|
230
|
+
"data-testid": "room-input",
|
|
231
|
+
"maxlength": "32",
|
|
232
|
+
"placeholder": "new-room",
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
self.room_form = ref(
|
|
237
|
+
form(
|
|
238
|
+
tw("flex gap-2 px-3 pb-4"),
|
|
239
|
+
self.room_input,
|
|
240
|
+
button(
|
|
241
|
+
tw(
|
|
242
|
+
"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 "
|
|
243
|
+
"text-lg font-medium text-white hover:bg-white/25 focus:outline-none "
|
|
244
|
+
"focus:ring-2 focus:ring-white/50"
|
|
245
|
+
),
|
|
246
|
+
{"aria-label": "Create room", "data-testid": "create-room", "type": "submit"},
|
|
247
|
+
"+",
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
self.room_form.addEventListener("submit", RoomCreateListener(self))
|
|
252
|
+
self.room_list = ref(
|
|
253
|
+
tag(
|
|
254
|
+
"nav",
|
|
255
|
+
tw("min-h-0 flex-1 space-y-1 overflow-y-auto px-2 pb-4"),
|
|
256
|
+
{"aria-label": "Rooms", "data-testid": "room-list"},
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
self.profile_name = ref(tag("span", tw("truncate font-medium"), "Guest"))
|
|
260
|
+
self.sign_out_button = ref(
|
|
261
|
+
button(
|
|
262
|
+
tw(
|
|
263
|
+
"rounded-md px-2 py-1 text-xs text-violet-200 hover:bg-white/10 "
|
|
264
|
+
"hover:text-white focus:outline-none focus:ring-2 focus:ring-white/40"
|
|
265
|
+
),
|
|
266
|
+
{"data-testid": "sign-out", "type": "button"},
|
|
267
|
+
"Change",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
self.sign_out_button.addEventListener("click", SignOutClickListener(self))
|
|
271
|
+
|
|
272
|
+
self.current_room = ref(tag("h2", tw("text-lg font-bold text-slate-950"), "# general"))
|
|
273
|
+
self.connection_status = ref(
|
|
274
|
+
tag("span", tw("text-xs font-medium text-slate-500"), "Not connected")
|
|
275
|
+
)
|
|
276
|
+
self.reconnect_button = ref(
|
|
277
|
+
button(
|
|
278
|
+
tw(
|
|
279
|
+
"hidden rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-semibold "
|
|
280
|
+
"text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 "
|
|
281
|
+
"focus:ring-violet-300"
|
|
282
|
+
),
|
|
283
|
+
{"data-testid": "reconnect", "type": "button"},
|
|
284
|
+
"Reconnect",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
self.reconnect_button.addEventListener("click", ReconnectClickListener(self))
|
|
288
|
+
self.error_banner = ref(
|
|
289
|
+
tag(
|
|
290
|
+
"div",
|
|
291
|
+
tw(
|
|
292
|
+
"hidden border-b border-red-200 bg-red-50 px-5 py-3 text-sm font-medium "
|
|
293
|
+
"text-red-700"
|
|
294
|
+
),
|
|
295
|
+
{"data-testid": "chat-error", "role": "alert"},
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
self.message_list = ref(
|
|
299
|
+
tag(
|
|
300
|
+
"section",
|
|
301
|
+
tw("min-h-0 flex-1 space-y-1 overflow-y-auto px-5 py-6"),
|
|
302
|
+
{"aria-label": "Messages", "data-testid": "message-list"},
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
self.message_input = ref(
|
|
306
|
+
input(
|
|
307
|
+
tw(
|
|
308
|
+
"min-w-0 flex-1 bg-transparent px-4 py-3 text-sm text-slate-950 outline-none "
|
|
309
|
+
"placeholder:text-slate-400"
|
|
310
|
+
),
|
|
311
|
+
{
|
|
312
|
+
"autocomplete": "off",
|
|
313
|
+
"data-testid": "message-input",
|
|
314
|
+
"maxlength": "2000",
|
|
315
|
+
"placeholder": "Message #general",
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
self.message_form = ref(
|
|
320
|
+
form(
|
|
321
|
+
tw(
|
|
322
|
+
"flex items-center rounded-xl border border-slate-300 bg-white shadow-sm "
|
|
323
|
+
"focus-within:border-violet-500 focus-within:ring-4 "
|
|
324
|
+
"focus-within:ring-violet-100"
|
|
325
|
+
),
|
|
326
|
+
self.message_input,
|
|
327
|
+
button(
|
|
328
|
+
tw(
|
|
329
|
+
"mr-2 rounded-lg bg-violet-600 px-4 py-2 text-sm font-semibold text-white "
|
|
330
|
+
"hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-violet-300"
|
|
331
|
+
),
|
|
332
|
+
{"data-testid": "send-message", "type": "submit"},
|
|
333
|
+
"Send",
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
self.message_form.addEventListener("submit", MessageSubmitListener(self))
|
|
338
|
+
|
|
339
|
+
self.member_count = ref(
|
|
340
|
+
tag(
|
|
341
|
+
"span",
|
|
342
|
+
tw("text-xs text-slate-500"),
|
|
343
|
+
{"data-testid": "member-count"},
|
|
344
|
+
"0 people",
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
self.member_list = ref(
|
|
348
|
+
tag(
|
|
349
|
+
"div",
|
|
350
|
+
tw("min-h-0 flex-1 space-y-1 overflow-y-auto p-3"),
|
|
351
|
+
{"data-testid": "member-list"},
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
self.workspace = ref(
|
|
356
|
+
tag(
|
|
357
|
+
"div",
|
|
358
|
+
tw("hidden h-screen min-h-[36rem] overflow-hidden bg-slate-100 text-slate-950"),
|
|
359
|
+
{"data-testid": "workspace"},
|
|
360
|
+
tag(
|
|
361
|
+
"div",
|
|
362
|
+
tw(
|
|
363
|
+
"grid h-full grid-cols-[16rem_minmax(0,1fr)] "
|
|
364
|
+
"lg:grid-cols-[16rem_minmax(0,1fr)_15rem]"
|
|
365
|
+
),
|
|
366
|
+
tag(
|
|
367
|
+
"aside",
|
|
368
|
+
tw("flex min-h-0 flex-col bg-violet-950 text-white"),
|
|
369
|
+
tag(
|
|
370
|
+
"header",
|
|
371
|
+
tw("border-b border-white/10 px-5 py-4"),
|
|
372
|
+
tag("p", tw("text-xs font-medium text-violet-300"), "WORKSPACE"),
|
|
373
|
+
tag(
|
|
374
|
+
"h1", tw("mt-1 text-xl font-bold tracking-tight"), "PyJS Community"
|
|
375
|
+
),
|
|
376
|
+
),
|
|
377
|
+
tag(
|
|
378
|
+
"div",
|
|
379
|
+
tw("flex items-center justify-between px-4 pb-2 pt-5"),
|
|
380
|
+
tag(
|
|
381
|
+
"h2",
|
|
382
|
+
tw(
|
|
383
|
+
"text-xs font-semibold uppercase tracking-wider text-violet-300"
|
|
384
|
+
),
|
|
385
|
+
"Rooms",
|
|
386
|
+
),
|
|
387
|
+
tag("span", tw("text-violet-300"), "+"),
|
|
388
|
+
),
|
|
389
|
+
self.room_form,
|
|
390
|
+
self.room_list,
|
|
391
|
+
tag(
|
|
392
|
+
"footer",
|
|
393
|
+
tw(
|
|
394
|
+
"flex items-center justify-between border-t border-white/10 "
|
|
395
|
+
"px-4 py-3 text-sm"
|
|
396
|
+
),
|
|
397
|
+
tag(
|
|
398
|
+
"div",
|
|
399
|
+
tw("flex min-w-0 items-center gap-2"),
|
|
400
|
+
tag("span", tw("h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-400")),
|
|
401
|
+
self.profile_name,
|
|
402
|
+
),
|
|
403
|
+
self.sign_out_button,
|
|
404
|
+
),
|
|
405
|
+
),
|
|
406
|
+
tag(
|
|
407
|
+
"main",
|
|
408
|
+
tw("flex min-h-0 min-w-0 flex-col bg-white"),
|
|
409
|
+
tag(
|
|
410
|
+
"header",
|
|
411
|
+
tw(
|
|
412
|
+
"flex h-[4.5rem] shrink-0 items-center justify-between border-b "
|
|
413
|
+
"border-slate-200 px-5 shadow-sm"
|
|
414
|
+
),
|
|
415
|
+
tag("div", self.current_room, self.connection_status),
|
|
416
|
+
self.reconnect_button,
|
|
417
|
+
),
|
|
418
|
+
self.error_banner,
|
|
419
|
+
self.message_list,
|
|
420
|
+
tag("div", tw("shrink-0 border-t border-slate-100 p-4"), self.message_form),
|
|
421
|
+
),
|
|
422
|
+
tag(
|
|
423
|
+
"aside",
|
|
424
|
+
tw("hidden min-h-0 flex-col border-l border-slate-200 bg-slate-50 lg:flex"),
|
|
425
|
+
tag(
|
|
426
|
+
"header",
|
|
427
|
+
tw(
|
|
428
|
+
"flex h-[4.5rem] shrink-0 items-center justify-between "
|
|
429
|
+
"border-b border-slate-200 px-4"
|
|
430
|
+
),
|
|
431
|
+
tag("h2", tw("font-bold text-slate-900"), "People"),
|
|
432
|
+
self.member_count,
|
|
433
|
+
),
|
|
434
|
+
self.member_list,
|
|
435
|
+
),
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
self.add(self.workspace, self.sign_in)
|
|
440
|
+
|
|
441
|
+
def initialize(self):
|
|
442
|
+
self.socket: WebSocket | None = None
|
|
443
|
+
name = sessionStorage.getItem("pyjs-chatroom-display-name")
|
|
444
|
+
if name is None:
|
|
445
|
+
self.show_sign_in()
|
|
446
|
+
return
|
|
447
|
+
if name == "":
|
|
448
|
+
self.show_sign_in()
|
|
449
|
+
return
|
|
450
|
+
self.name = name
|
|
451
|
+
self.show_workspace()
|
|
452
|
+
self.connect()
|
|
453
|
+
|
|
454
|
+
@effect(name)
|
|
455
|
+
def render_name(self):
|
|
456
|
+
self.profile_name.textContent = self.name if self.name != "" else "Guest"
|
|
457
|
+
|
|
458
|
+
@effect(room)
|
|
459
|
+
def render_room(self):
|
|
460
|
+
self.current_room.textContent = "# " + self.room
|
|
461
|
+
self.message_input.setAttribute("placeholder", "Message #" + self.room)
|
|
462
|
+
|
|
463
|
+
@effect(connection)
|
|
464
|
+
def render_connection(self):
|
|
465
|
+
if self.connection == "connecting":
|
|
466
|
+
self.connection_status.textContent = "Connecting…"
|
|
467
|
+
elif self.connection == "connected":
|
|
468
|
+
self.connection_status.textContent = "Connected"
|
|
469
|
+
elif self.connection == "disconnected":
|
|
470
|
+
self.connection_status.textContent = "Disconnected"
|
|
471
|
+
elif self.connection == "preview":
|
|
472
|
+
self.connection_status.textContent = "Preview mode"
|
|
473
|
+
else:
|
|
474
|
+
self.connection_status.textContent = "Not connected"
|
|
475
|
+
if self.connection == "disconnected":
|
|
476
|
+
self.reconnect_button.classList.remove("hidden")
|
|
477
|
+
else:
|
|
478
|
+
self.reconnect_button.classList.add("hidden")
|
|
479
|
+
|
|
480
|
+
def show_sign_in(self):
|
|
481
|
+
self.workspace.classList.add("hidden")
|
|
482
|
+
self.sign_in.classList.remove("hidden")
|
|
483
|
+
self.connection = "signed-out"
|
|
484
|
+
self.name_input.focus()
|
|
485
|
+
|
|
486
|
+
def show_workspace(self):
|
|
487
|
+
self.sign_in.classList.add("hidden")
|
|
488
|
+
self.workspace.classList.remove("hidden")
|
|
489
|
+
|
|
490
|
+
def submit_name(self):
|
|
491
|
+
name = self.name_input.value.strip()
|
|
492
|
+
if name == "":
|
|
493
|
+
self.show_error("Enter a display name to join the workspace.")
|
|
494
|
+
return
|
|
495
|
+
sessionStorage.setItem("pyjs-chatroom-display-name", name)
|
|
496
|
+
self.name = name
|
|
497
|
+
self.show_workspace()
|
|
498
|
+
self.connect()
|
|
499
|
+
|
|
500
|
+
def sign_out(self):
|
|
501
|
+
sessionStorage.removeItem("pyjs-chatroom-display-name")
|
|
502
|
+
socket = self.socket
|
|
503
|
+
if self.connection == "connected":
|
|
504
|
+
if socket is not None:
|
|
505
|
+
socket.close()
|
|
506
|
+
self.name = ""
|
|
507
|
+
self.name_input.value = ""
|
|
508
|
+
self.show_sign_in()
|
|
509
|
+
|
|
510
|
+
def connect(self):
|
|
511
|
+
if self.socket_path == "":
|
|
512
|
+
self.connection = "preview"
|
|
513
|
+
return
|
|
514
|
+
self.connection = "connecting"
|
|
515
|
+
socket = WebSocket("ws://" + location.host + self.socket_path)
|
|
516
|
+
self.socket = socket
|
|
517
|
+
socket.addEventListener("open", SocketOpenListener(self))
|
|
518
|
+
socket.addEventListener("message", SocketMessageListener(self))
|
|
519
|
+
socket.addEventListener("close", SocketCloseListener(self))
|
|
520
|
+
socket.addEventListener("error", SocketErrorListener(self))
|
|
521
|
+
|
|
522
|
+
def socket_opened(self):
|
|
523
|
+
self.connection = "connected"
|
|
524
|
+
self.send(OutgoingChatMessage("join", self.name, self.room, ""))
|
|
525
|
+
|
|
526
|
+
def socket_closed(self):
|
|
527
|
+
if self.connection == "signed-out":
|
|
528
|
+
return
|
|
529
|
+
self.connection = "disconnected"
|
|
530
|
+
|
|
531
|
+
def send(self, message: OutgoingChatMessage):
|
|
532
|
+
socket = self.socket
|
|
533
|
+
if self.connection == "connected":
|
|
534
|
+
if socket is not None:
|
|
535
|
+
socket.send(JSON.stringify(message))
|
|
536
|
+
|
|
537
|
+
def receive(self, message: ChatSnapshot):
|
|
538
|
+
if message.type == "error":
|
|
539
|
+
self.show_error(message.message)
|
|
540
|
+
return
|
|
541
|
+
self.hide_error()
|
|
542
|
+
self.room = message.room
|
|
543
|
+
self.render_rooms(message.rooms)
|
|
544
|
+
self.render_messages(message.messages)
|
|
545
|
+
self.render_members(message.users)
|
|
546
|
+
|
|
547
|
+
def render_rooms(self, rooms: list[str]):
|
|
548
|
+
self.room_list.replaceChildren()
|
|
549
|
+
current_room = self.room
|
|
550
|
+
for room in rooms:
|
|
551
|
+
if room == current_room:
|
|
552
|
+
room_button = button(
|
|
553
|
+
tw(
|
|
554
|
+
"flex w-full items-center gap-2 rounded-lg bg-white/15 px-3 py-2 "
|
|
555
|
+
"text-left text-sm font-semibold text-white"
|
|
556
|
+
),
|
|
557
|
+
{"data-room": room, "type": "button"},
|
|
558
|
+
tag("span", tw("text-violet-300"), "#"),
|
|
559
|
+
room,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
room_button = button(
|
|
563
|
+
tw(
|
|
564
|
+
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm "
|
|
565
|
+
"font-medium text-violet-100 hover:bg-white/10 hover:text-white"
|
|
566
|
+
),
|
|
567
|
+
{"data-room": room, "type": "button"},
|
|
568
|
+
tag("span", tw("text-violet-400"), "#"),
|
|
569
|
+
room,
|
|
570
|
+
)
|
|
571
|
+
room_button.addEventListener("click", RoomClickListener(self, room))
|
|
572
|
+
self.room_list.append(room_button)
|
|
573
|
+
|
|
574
|
+
def render_messages(self, messages: list[ChatMessage]):
|
|
575
|
+
self.message_list.replaceChildren()
|
|
576
|
+
if len(messages) == 0:
|
|
577
|
+
self.message_list.append(
|
|
578
|
+
tag(
|
|
579
|
+
"div",
|
|
580
|
+
tw("flex h-full min-h-64 flex-col items-center justify-center text-center"),
|
|
581
|
+
tag(
|
|
582
|
+
"div",
|
|
583
|
+
tw(
|
|
584
|
+
"flex h-14 w-14 items-center justify-center rounded-2xl bg-violet-100 "
|
|
585
|
+
"text-2xl font-bold text-violet-700"
|
|
586
|
+
),
|
|
587
|
+
"#",
|
|
588
|
+
),
|
|
589
|
+
tag(
|
|
590
|
+
"h3",
|
|
591
|
+
tw("mt-4 text-lg font-bold text-slate-900"),
|
|
592
|
+
"This is the start of #" + self.room,
|
|
593
|
+
),
|
|
594
|
+
tag(
|
|
595
|
+
"p",
|
|
596
|
+
tw("mt-1 max-w-sm text-sm text-slate-500"),
|
|
597
|
+
"Say hello and start the conversation.",
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
return
|
|
602
|
+
for message in messages:
|
|
603
|
+
self.message_list.append(
|
|
604
|
+
tag(
|
|
605
|
+
"article",
|
|
606
|
+
tw("group flex gap-3 rounded-lg px-2 py-2 hover:bg-slate-50"),
|
|
607
|
+
{"data-message-id": str(message.id)},
|
|
608
|
+
tag(
|
|
609
|
+
"div",
|
|
610
|
+
tw(
|
|
611
|
+
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg "
|
|
612
|
+
"bg-violet-100 text-sm font-bold text-violet-700"
|
|
613
|
+
),
|
|
614
|
+
"●",
|
|
615
|
+
),
|
|
616
|
+
tag(
|
|
617
|
+
"div",
|
|
618
|
+
tw("min-w-0 flex-1"),
|
|
619
|
+
tag(
|
|
620
|
+
"div",
|
|
621
|
+
tw("flex items-baseline gap-2"),
|
|
622
|
+
tag("strong", tw("text-sm font-bold text-slate-900"), message.sender),
|
|
623
|
+
tag("time", tw("text-[0.7rem] text-slate-400"), message.time),
|
|
624
|
+
),
|
|
625
|
+
tag(
|
|
626
|
+
"p",
|
|
627
|
+
tw(
|
|
628
|
+
"mt-0.5 whitespace-pre-wrap break-words text-sm leading-6 "
|
|
629
|
+
"text-slate-700"
|
|
630
|
+
),
|
|
631
|
+
message.text,
|
|
632
|
+
),
|
|
633
|
+
),
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
self.message_list.scrollTop = self.message_list.scrollHeight
|
|
637
|
+
|
|
638
|
+
def render_members(self, users: list[str]):
|
|
639
|
+
self.member_list.replaceChildren()
|
|
640
|
+
count = len(users)
|
|
641
|
+
self.member_count.textContent = str(count) + (" person" if count == 1 else " people")
|
|
642
|
+
for user in users:
|
|
643
|
+
self.member_list.append(
|
|
644
|
+
tag(
|
|
645
|
+
"div",
|
|
646
|
+
tw("flex items-center gap-3 rounded-lg px-2 py-2 text-sm hover:bg-white"),
|
|
647
|
+
{"data-user": user},
|
|
648
|
+
tag("span", tw("h-2.5 w-2.5 rounded-full bg-emerald-500")),
|
|
649
|
+
tag("span", tw("truncate font-medium text-slate-700"), user),
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def switch_room(self, room: str):
|
|
654
|
+
self.send(OutgoingChatMessage("switch_room", self.name, room, ""))
|
|
655
|
+
|
|
656
|
+
def create_room(self):
|
|
657
|
+
room = self.room_input.value.strip()
|
|
658
|
+
if room == "":
|
|
659
|
+
return
|
|
660
|
+
self.room_input.value = ""
|
|
661
|
+
self.send(OutgoingChatMessage("create_room", self.name, room, ""))
|
|
662
|
+
|
|
663
|
+
def submit_message(self):
|
|
664
|
+
text = self.message_input.value.strip()
|
|
665
|
+
if text == "":
|
|
666
|
+
return
|
|
667
|
+
self.message_input.value = ""
|
|
668
|
+
self.send(OutgoingChatMessage("message", self.name, self.room, text))
|
|
669
|
+
|
|
670
|
+
def show_error(self, message: str):
|
|
671
|
+
self.error_banner.textContent = message
|
|
672
|
+
self.error_banner.classList.remove("hidden")
|
|
673
|
+
|
|
674
|
+
def hide_error(self):
|
|
675
|
+
self.error_banner.classList.add("hidden")
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def main(socket_path: str = ""):
|
|
679
|
+
return ChatRoomApp(socket_path=socket_path)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@nojs
|
|
683
|
+
def render(request):
|
|
684
|
+
return main(socket_path="/ws")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@nojs
|
|
688
|
+
async def websocket(connection):
|
|
689
|
+
from .chat_server import chat_server
|
|
690
|
+
|
|
691
|
+
await chat_server.handle(connection)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
if __name__ == "__main__":
|
|
695
|
+
import os
|
|
696
|
+
|
|
697
|
+
from pyjs.server import serve
|
|
698
|
+
|
|
699
|
+
serve(
|
|
700
|
+
"chatroom",
|
|
701
|
+
render=render,
|
|
702
|
+
websocket_routes={"/ws": websocket},
|
|
703
|
+
port=int(os.environ.get("PYJS_PORT", "8000")),
|
|
704
|
+
)
|