jac-client 0.1.0__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.
- jac_client/docs/README.md +629 -0
- jac_client/docs/advanced-state.md +706 -0
- jac_client/docs/imports.md +650 -0
- jac_client/docs/lifecycle-hooks.md +554 -0
- jac_client/docs/routing.md +530 -0
- jac_client/examples/little-x/app.jac +615 -0
- jac_client/examples/little-x/package-lock.json +2840 -0
- jac_client/examples/little-x/package.json +23 -0
- jac_client/examples/little-x/submit-button.jac +8 -0
- jac_client/examples/todo-app/README.md +82 -0
- jac_client/examples/todo-app/app.jac +683 -0
- jac_client/examples/todo-app/package-lock.json +999 -0
- jac_client/examples/todo-app/package.json +22 -0
- jac_client/plugin/cli.py +328 -0
- jac_client/plugin/client.py +41 -0
- jac_client/plugin/client_runtime.jac +941 -0
- jac_client/plugin/vite_client_bundle.py +470 -0
- jac_client/tests/__init__.py +2 -0
- jac_client/tests/fixtures/button.jac +6 -0
- jac_client/tests/fixtures/client_app.jac +18 -0
- jac_client/tests/fixtures/client_app_with_antd.jac +21 -0
- jac_client/tests/fixtures/js_import.jac +30 -0
- jac_client/tests/fixtures/package-lock.json +329 -0
- jac_client/tests/fixtures/package.json +11 -0
- jac_client/tests/fixtures/relative_import.jac +13 -0
- jac_client/tests/fixtures/test_fragments_spread.jac +44 -0
- jac_client/tests/fixtures/utils.js +22 -0
- jac_client/tests/test_cl.py +360 -0
- jac_client/tests/test_create_jac_app.py +139 -0
- jac_client-0.1.0.dist-info/METADATA +126 -0
- jac_client-0.1.0.dist-info/RECORD +33 -0
- jac_client-0.1.0.dist-info/WHEEL +4 -0
- jac_client-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
node Todo {
|
|
2
|
+
has text: str;
|
|
3
|
+
has done: bool = False;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
walker create_todo {
|
|
7
|
+
can create with `root entry {
|
|
8
|
+
new_todo = here ++> Todo(text="Example Todo");
|
|
9
|
+
report new_todo;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
walker toggle_todo {
|
|
13
|
+
can toggle with Todo entry {
|
|
14
|
+
here.done = not here.done;
|
|
15
|
+
report here;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
walker read_todos {
|
|
19
|
+
can read with `root entry {
|
|
20
|
+
visit [-->(`?Todo)];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
can report_todos with exit {
|
|
24
|
+
report here;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cl {
|
|
29
|
+
let [todoState, setTodoState] = createState({
|
|
30
|
+
"items": [],
|
|
31
|
+
"filter": "all",
|
|
32
|
+
"input": ""
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
# -----------------------------
|
|
36
|
+
# State helpers
|
|
37
|
+
# -----------------------------
|
|
38
|
+
def onInputChange(e: any) -> None {
|
|
39
|
+
setTodoState({"input": e.target.value});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async def onAddTodo(e: any) -> None {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
inputEl = document.getElementById("todo-input");
|
|
45
|
+
text = (inputEl.value if inputEl and inputEl.value else "").trim();
|
|
46
|
+
if not text { return; }
|
|
47
|
+
|
|
48
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
49
|
+
|
|
50
|
+
s = todoState();
|
|
51
|
+
newItem = {"id": new_todo._jac_id, "text": new_todo.text, "done": new_todo.done};
|
|
52
|
+
setTodoState({"items": s.items.concat([newItem])});
|
|
53
|
+
|
|
54
|
+
# Clear the input visually
|
|
55
|
+
if inputEl { inputEl.value = ""; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def setFilter(next: str) -> None {
|
|
59
|
+
setTodoState({"filter": next});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async def toggleTodo(id: any) -> None {
|
|
63
|
+
# we need to pass the id to the toggle_todo walker
|
|
64
|
+
toggled_todo = await __jacSpawn("toggle_todo", {}, id);
|
|
65
|
+
s = todoState();
|
|
66
|
+
updated = [];
|
|
67
|
+
for item in s.items {
|
|
68
|
+
if item.id == id {
|
|
69
|
+
updated.push({"id": item.id, "text": item.text, "done": not item.done});
|
|
70
|
+
} else {
|
|
71
|
+
updated.push(item);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setTodoState({"items": updated});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def removeTodo(id: any) -> None {
|
|
78
|
+
s = todoState();
|
|
79
|
+
remaining = [];
|
|
80
|
+
for item in s.items {
|
|
81
|
+
if item.id != id { remaining.push(item); }
|
|
82
|
+
}
|
|
83
|
+
setTodoState({"items": remaining});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def clearCompleted() -> None {
|
|
87
|
+
s = todoState();
|
|
88
|
+
remaining = [];
|
|
89
|
+
for item in s.items {
|
|
90
|
+
if not item.done { remaining.push(item); }
|
|
91
|
+
}
|
|
92
|
+
setTodoState({"items": remaining});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def filteredItems() -> list {
|
|
96
|
+
s = todoState();
|
|
97
|
+
result = [];
|
|
98
|
+
if s.filter == "active" {
|
|
99
|
+
for it in s.items { if not it.done { result.push(it); } }
|
|
100
|
+
return result;
|
|
101
|
+
} elif s.filter == "completed" {
|
|
102
|
+
for it in s.items { if it.done { result.push(it); } }
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
return s.items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# -----------------------------
|
|
109
|
+
# UI pieces
|
|
110
|
+
# -----------------------------
|
|
111
|
+
def TodoItem(item: dict) -> any {
|
|
112
|
+
console.log(item.text,"-",typeof (item.done)== 'boolean');
|
|
113
|
+
return <li key={item.id} style={{
|
|
114
|
+
"display": "flex",
|
|
115
|
+
"gap": "12px",
|
|
116
|
+
"alignItems": "center",
|
|
117
|
+
"background": "#FFFFFF",
|
|
118
|
+
"padding": "12px 16px",
|
|
119
|
+
"borderRadius": "10px",
|
|
120
|
+
"marginBottom": "8px",
|
|
121
|
+
"boxShadow": "0 1px 2px rgba(17,24,39,0.06)",
|
|
122
|
+
"border": "1px solid #E5E7EB"
|
|
123
|
+
}}>
|
|
124
|
+
<input
|
|
125
|
+
type="checkbox"
|
|
126
|
+
checked={item.done}
|
|
127
|
+
|
|
128
|
+
onChange={lambda -> None { toggleTodo(item.id); }}
|
|
129
|
+
style={{
|
|
130
|
+
"width": "18px",
|
|
131
|
+
"height": "18px",
|
|
132
|
+
"accentColor": "#7C3AED",
|
|
133
|
+
"cursor": "pointer"
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
<span style={{
|
|
137
|
+
"textDecoration": ("line-through" if item.done else "none"),
|
|
138
|
+
"flex": "1",
|
|
139
|
+
"fontSize": "16px",
|
|
140
|
+
"color": ("#9CA3AF" if item.done else "#111827")
|
|
141
|
+
}}>
|
|
142
|
+
{item.text}
|
|
143
|
+
</span>
|
|
144
|
+
<button
|
|
145
|
+
style={{
|
|
146
|
+
"marginLeft": "auto",
|
|
147
|
+
"padding": "6px 12px",
|
|
148
|
+
"background": "#FFFFFF",
|
|
149
|
+
"color": "#EF4444",
|
|
150
|
+
"border": "1px solid #FCA5A5",
|
|
151
|
+
"borderRadius": "6px",
|
|
152
|
+
"fontSize": "12px",
|
|
153
|
+
"fontWeight": "600",
|
|
154
|
+
"cursor": "pointer",
|
|
155
|
+
"boxShadow": "none",
|
|
156
|
+
"transition": "all 0.2s ease"
|
|
157
|
+
}}
|
|
158
|
+
onClick={lambda -> None { removeTodo(item.id); }}>
|
|
159
|
+
Remove
|
|
160
|
+
</button>
|
|
161
|
+
</li>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def RenderUl(children: list, style: dict = {}) -> any {
|
|
165
|
+
return __jacJsx("ul", {"style": style}, children);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async def read_todos_action() -> any {
|
|
169
|
+
todos = await __jacSpawn("read_todos");
|
|
170
|
+
for todo in todos.reports {
|
|
171
|
+
console.log("Todo read:", todo);
|
|
172
|
+
setTodoState({"items": todoState().items.concat([{"id": todo._jac_id, "text": todo.text, "done": todo.done}])});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def TodoApp() -> any {
|
|
177
|
+
s = todoState();
|
|
178
|
+
|
|
179
|
+
onMount(lambda -> None {
|
|
180
|
+
read_todos_action();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
itemsArr = filteredItems();
|
|
185
|
+
if not Array.isArray(itemsArr) {
|
|
186
|
+
console.warn("filteredItems() did not return an array; coercing to []", itemsArr);
|
|
187
|
+
itemsArr = [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
activeCount = 0;
|
|
191
|
+
for it in s.items { if not it.done { activeCount += 1; } }
|
|
192
|
+
|
|
193
|
+
children = [];
|
|
194
|
+
for it in itemsArr { children.push(TodoItem(it)); }
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
return <div style={{
|
|
200
|
+
"maxWidth": "640px",
|
|
201
|
+
"margin": "24px auto",
|
|
202
|
+
"padding": "24px",
|
|
203
|
+
"background": "#FFFFFF",
|
|
204
|
+
"borderRadius": "12px",
|
|
205
|
+
"fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
206
|
+
"boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
|
|
207
|
+
"border": "1px solid #E5E7EB",
|
|
208
|
+
"color": "#111827"
|
|
209
|
+
}}>
|
|
210
|
+
<h2 style={{
|
|
211
|
+
"marginTop": "0",
|
|
212
|
+
"color": "#111827",
|
|
213
|
+
"textAlign": "left",
|
|
214
|
+
"fontSize": "1.5rem",
|
|
215
|
+
"fontWeight": "700",
|
|
216
|
+
"marginBottom": "16px"
|
|
217
|
+
}}>My Todos</h2>
|
|
218
|
+
|
|
219
|
+
<form id="todo_submit" onSubmit={onAddTodo} style={{
|
|
220
|
+
"display": "flex",
|
|
221
|
+
"gap": "12px",
|
|
222
|
+
"marginBottom": "16px",
|
|
223
|
+
"background": "#FFFFFF",
|
|
224
|
+
"padding": "16px",
|
|
225
|
+
"borderRadius": "10px",
|
|
226
|
+
"border": "1px solid #E5E7EB",
|
|
227
|
+
"boxShadow": "0 1px 2px rgba(17,24,39,0.04)"
|
|
228
|
+
}}>
|
|
229
|
+
<input
|
|
230
|
+
id="todo-input"
|
|
231
|
+
type="text"
|
|
232
|
+
placeholder="What needs to be done?"
|
|
233
|
+
style={{
|
|
234
|
+
"flex": "1",
|
|
235
|
+
"padding": "12px 14px",
|
|
236
|
+
"border": "1px solid #E5E7EB",
|
|
237
|
+
"borderRadius": "8px",
|
|
238
|
+
"fontSize": "16px",
|
|
239
|
+
"outline": "none",
|
|
240
|
+
"background": "#FFFFFF",
|
|
241
|
+
"color": "#111827"
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
<button type="submit" style={{
|
|
245
|
+
"padding": "12px 18px",
|
|
246
|
+
"background": "#7C3AED",
|
|
247
|
+
"color": "#FFFFFF",
|
|
248
|
+
"border": "1px solid #6D28D9",
|
|
249
|
+
"borderRadius": "8px",
|
|
250
|
+
"fontSize": "15px",
|
|
251
|
+
"fontWeight": "600",
|
|
252
|
+
"cursor": "pointer",
|
|
253
|
+
"boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
|
|
254
|
+
"transition": "all 0.2s ease"
|
|
255
|
+
}}>Add Todo</button>
|
|
256
|
+
</form>
|
|
257
|
+
|
|
258
|
+
<div style={{
|
|
259
|
+
"display": "flex",
|
|
260
|
+
"gap": "8px",
|
|
261
|
+
"marginTop": "8px",
|
|
262
|
+
"background": "#FFFFFF",
|
|
263
|
+
"padding": "10px",
|
|
264
|
+
"borderRadius": "10px",
|
|
265
|
+
"border": "1px solid #E5E7EB",
|
|
266
|
+
"flexWrap": "wrap"
|
|
267
|
+
}}>
|
|
268
|
+
<button
|
|
269
|
+
onClick={lambda -> None { setFilter("all"); }}
|
|
270
|
+
style={{
|
|
271
|
+
"padding": "8px 14px",
|
|
272
|
+
"background": ("#7C3AED" if s.filter == "all" else "#FFFFFF"),
|
|
273
|
+
"color": ("#FFFFFF" if s.filter == "all" else "#7C3AED"),
|
|
274
|
+
"border": ("1px solid #6D28D9" if s.filter == "all" else "1px solid #E5E7EB"),
|
|
275
|
+
"borderRadius": "20px",
|
|
276
|
+
"fontSize": "14px",
|
|
277
|
+
"fontWeight": ("700" if s.filter == "all" else "500"),
|
|
278
|
+
"cursor": "pointer",
|
|
279
|
+
"boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "all" else "none"),
|
|
280
|
+
"transition": "all 0.2s ease"
|
|
281
|
+
}}>
|
|
282
|
+
All
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
onClick={lambda -> None { setFilter("active"); }}
|
|
286
|
+
style={{
|
|
287
|
+
"padding": "8px 14px",
|
|
288
|
+
"background": ("#7C3AED" if s.filter == "active" else "#FFFFFF"),
|
|
289
|
+
"color": ("#FFFFFF" if s.filter == "active" else "#7C3AED"),
|
|
290
|
+
"border": ("1px solid #6D28D9" if s.filter == "active" else "1px solid #E5E7EB"),
|
|
291
|
+
"borderRadius": "20px",
|
|
292
|
+
"fontSize": "14px",
|
|
293
|
+
"fontWeight": ("700" if s.filter == "active" else "500"),
|
|
294
|
+
"cursor": "pointer",
|
|
295
|
+
"boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "active" else "none"),
|
|
296
|
+
"transition": "all 0.2s ease"
|
|
297
|
+
}}>
|
|
298
|
+
Active
|
|
299
|
+
</button>
|
|
300
|
+
<button
|
|
301
|
+
onClick={lambda -> None { setFilter("completed"); }}
|
|
302
|
+
style={{
|
|
303
|
+
"padding": "8px 14px",
|
|
304
|
+
"background": ("#7C3AED" if s.filter == "completed" else "#FFFFFF"),
|
|
305
|
+
"color": ("#FFFFFF" if s.filter == "completed" else "#7C3AED"),
|
|
306
|
+
"border": ("1px solid #6D28D9" if s.filter == "completed" else "1px solid #E5E7EB"),
|
|
307
|
+
"borderRadius": "20px",
|
|
308
|
+
"fontSize": "14px",
|
|
309
|
+
"fontWeight": ("700" if s.filter == "completed" else "500"),
|
|
310
|
+
"cursor": "pointer",
|
|
311
|
+
"boxShadow": ("0 1px 2px rgba(124,58,237,0.25)" if s.filter == "completed" else "none"),
|
|
312
|
+
"transition": "all 0.2s ease"
|
|
313
|
+
}}>
|
|
314
|
+
Completed
|
|
315
|
+
</button>
|
|
316
|
+
<button
|
|
317
|
+
style={{
|
|
318
|
+
"marginLeft": "auto",
|
|
319
|
+
"padding": "8px 14px",
|
|
320
|
+
"background": "#F9FAFB",
|
|
321
|
+
"color": "#374151",
|
|
322
|
+
"border": "1px solid #E5E7EB",
|
|
323
|
+
"borderRadius": "20px",
|
|
324
|
+
"fontSize": "14px",
|
|
325
|
+
"fontWeight": "600",
|
|
326
|
+
"cursor": "pointer",
|
|
327
|
+
"boxShadow": "none",
|
|
328
|
+
"transition": "all 0.2s ease"
|
|
329
|
+
}}
|
|
330
|
+
onClick={clearCompleted}>
|
|
331
|
+
Clear Completed
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{RenderUl(children, {
|
|
336
|
+
"listStyle": "none",
|
|
337
|
+
"padding": "0",
|
|
338
|
+
"marginTop": "12px",
|
|
339
|
+
"background": "#FFFFFF",
|
|
340
|
+
"borderRadius": "12px",
|
|
341
|
+
"padding": "12px",
|
|
342
|
+
"border": "1px solid #E5E7EB",
|
|
343
|
+
"max-height": "366px",
|
|
344
|
+
"overflowY": "auto"
|
|
345
|
+
})}
|
|
346
|
+
{[TodoItem(item) for item in items]}
|
|
347
|
+
<div style={{
|
|
348
|
+
"marginTop": "12px",
|
|
349
|
+
"color": "#374151",
|
|
350
|
+
"textAlign": "center",
|
|
351
|
+
"fontSize": "14px",
|
|
352
|
+
"fontWeight": "500",
|
|
353
|
+
"background": "#F3F4F6",
|
|
354
|
+
"padding": "8px 12px",
|
|
355
|
+
"borderRadius": "10px",
|
|
356
|
+
"border": "1px solid #E5E7EB"
|
|
357
|
+
}}>
|
|
358
|
+
{s.items.length} total, {activeCount} active
|
|
359
|
+
</div>
|
|
360
|
+
</div>;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# -----------------------------
|
|
364
|
+
# Auth pages
|
|
365
|
+
# -----------------------------
|
|
366
|
+
def LoginForm() -> any {
|
|
367
|
+
return <div style={{
|
|
368
|
+
"maxWidth": "420px",
|
|
369
|
+
"margin": "60px auto",
|
|
370
|
+
"padding": "28px",
|
|
371
|
+
"background": "#FFFFFF",
|
|
372
|
+
"borderRadius": "12px",
|
|
373
|
+
"fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
374
|
+
"boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
|
|
375
|
+
"border": "1px solid #E5E7EB",
|
|
376
|
+
"color": "#111827"
|
|
377
|
+
}}>
|
|
378
|
+
<h2 style={{
|
|
379
|
+
"marginTop": "0",
|
|
380
|
+
"color": "#111827",
|
|
381
|
+
"textAlign": "center",
|
|
382
|
+
"fontSize": "1.5rem",
|
|
383
|
+
"fontWeight": "700",
|
|
384
|
+
"marginBottom": "20px"
|
|
385
|
+
}}>Welcome Back</h2>
|
|
386
|
+
<form onSubmit={handle_login}>
|
|
387
|
+
<div style={{"marginBottom": "20px"}}>
|
|
388
|
+
<label style={{
|
|
389
|
+
"display": "block",
|
|
390
|
+
"marginBottom": "8px",
|
|
391
|
+
"color": "#374151",
|
|
392
|
+
"fontSize": "14px",
|
|
393
|
+
"fontWeight": "600"
|
|
394
|
+
}}>Username</label>
|
|
395
|
+
<input id="login-username" type="text" style={{
|
|
396
|
+
"width": "100%",
|
|
397
|
+
"padding": "12px 16px",
|
|
398
|
+
"border": "1px solid #E5E7EB",
|
|
399
|
+
"borderRadius": "8px",
|
|
400
|
+
"fontSize": "16px",
|
|
401
|
+
"background": "#FFFFFF",
|
|
402
|
+
"color": "#111827",
|
|
403
|
+
"outline": "none",
|
|
404
|
+
"boxSizing": "border-box"
|
|
405
|
+
}}/>
|
|
406
|
+
</div>
|
|
407
|
+
<div style={{"marginBottom": "24px"}}>
|
|
408
|
+
<label style={{
|
|
409
|
+
"display": "block",
|
|
410
|
+
"marginBottom": "8px",
|
|
411
|
+
"color": "#374151",
|
|
412
|
+
"fontSize": "14px",
|
|
413
|
+
"fontWeight": "600"
|
|
414
|
+
}}>Password</label>
|
|
415
|
+
<input id="login-password" type="password" style={{
|
|
416
|
+
"width": "100%",
|
|
417
|
+
"padding": "12px 16px",
|
|
418
|
+
"border": "1px solid #E5E7EB",
|
|
419
|
+
"borderRadius": "8px",
|
|
420
|
+
"fontSize": "16px",
|
|
421
|
+
"background": "#FFFFFF",
|
|
422
|
+
"color": "#111827",
|
|
423
|
+
"outline": "none",
|
|
424
|
+
"boxSizing": "border-box"
|
|
425
|
+
}}/>
|
|
426
|
+
</div>
|
|
427
|
+
<button type="submit" style={{
|
|
428
|
+
"width": "100%",
|
|
429
|
+
"padding": "12px",
|
|
430
|
+
"background": "#7C3AED",
|
|
431
|
+
"color": "#FFFFFF",
|
|
432
|
+
"border": "1px solid #6D28D9",
|
|
433
|
+
"borderRadius": "8px",
|
|
434
|
+
"fontSize": "15px",
|
|
435
|
+
"fontWeight": "600",
|
|
436
|
+
"cursor": "pointer",
|
|
437
|
+
"boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
|
|
438
|
+
"transition": "all 0.2s ease"
|
|
439
|
+
}}>Sign In</button>
|
|
440
|
+
</form>
|
|
441
|
+
<div style={{
|
|
442
|
+
"marginTop": "16px",
|
|
443
|
+
"textAlign": "center",
|
|
444
|
+
"background": "#F9FAFB",
|
|
445
|
+
"padding": "10px",
|
|
446
|
+
"borderRadius": "8px",
|
|
447
|
+
"border": "1px solid #E5E7EB"
|
|
448
|
+
}}>
|
|
449
|
+
<Link href="/signup" style={{
|
|
450
|
+
"color": "#7C3AED",
|
|
451
|
+
"textDecoration": "none",
|
|
452
|
+
"fontWeight": "500"
|
|
453
|
+
}}>Create an account</Link>
|
|
454
|
+
</div>
|
|
455
|
+
</div>;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async def handle_login(e: any) -> None {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
username = document.getElementById("login-username").value;
|
|
461
|
+
password = document.getElementById("login-password").value;
|
|
462
|
+
success = await jacLogin(username, password);
|
|
463
|
+
if success { navigate("/todos"); } else { alert("Login failed"); }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
def SignupForm() -> any {
|
|
467
|
+
return <div style={{
|
|
468
|
+
"maxWidth": "420px",
|
|
469
|
+
"margin": "60px auto",
|
|
470
|
+
"padding": "28px",
|
|
471
|
+
"background": "#FFFFFF",
|
|
472
|
+
"borderRadius": "12px",
|
|
473
|
+
"fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
474
|
+
"boxShadow": "0 10px 20px rgba(17,24,39,0.06)",
|
|
475
|
+
"border": "1px solid #E5E7EB",
|
|
476
|
+
"color": "#111827"
|
|
477
|
+
}}>
|
|
478
|
+
<h2 style={{
|
|
479
|
+
"marginTop": "0",
|
|
480
|
+
"color": "#111827",
|
|
481
|
+
"textAlign": "center",
|
|
482
|
+
"fontSize": "1.5rem",
|
|
483
|
+
"fontWeight": "700",
|
|
484
|
+
"marginBottom": "20px"
|
|
485
|
+
}}>Join Us</h2>
|
|
486
|
+
<form onSubmit={handle_signup}>
|
|
487
|
+
<div style={{"marginBottom": "20px"}}>
|
|
488
|
+
<label style={{
|
|
489
|
+
"display": "block",
|
|
490
|
+
"marginBottom": "8px",
|
|
491
|
+
"color": "#374151",
|
|
492
|
+
"fontSize": "14px",
|
|
493
|
+
"fontWeight": "600"
|
|
494
|
+
}}>Username</label>
|
|
495
|
+
<input id="signup-username" type="text" required style={{
|
|
496
|
+
"width": "100%",
|
|
497
|
+
"padding": "12px 16px",
|
|
498
|
+
"border": "1px solid #E5E7EB",
|
|
499
|
+
"borderRadius": "8px",
|
|
500
|
+
"fontSize": "16px",
|
|
501
|
+
"background": "#FFFFFF",
|
|
502
|
+
"color": "#111827",
|
|
503
|
+
"outline": "none",
|
|
504
|
+
"boxSizing": "border-box"
|
|
505
|
+
}}/>
|
|
506
|
+
</div>
|
|
507
|
+
<div style={{"marginBottom": "20px"}}>
|
|
508
|
+
<label style={{
|
|
509
|
+
"display": "block",
|
|
510
|
+
"marginBottom": "8px",
|
|
511
|
+
"color": "#374151",
|
|
512
|
+
"fontSize": "14px",
|
|
513
|
+
"fontWeight": "600"
|
|
514
|
+
}}>Password</label>
|
|
515
|
+
<input id="signup-password" type="password" required style={{
|
|
516
|
+
"width": "100%",
|
|
517
|
+
"padding": "12px 16px",
|
|
518
|
+
"border": "1px solid #E5E7EB",
|
|
519
|
+
"borderRadius": "8px",
|
|
520
|
+
"fontSize": "16px",
|
|
521
|
+
"background": "#FFFFFF",
|
|
522
|
+
"color": "#111827",
|
|
523
|
+
"outline": "none",
|
|
524
|
+
"boxSizing": "border-box"
|
|
525
|
+
}}/>
|
|
526
|
+
</div>
|
|
527
|
+
<div style={{"marginBottom": "24px"}}>
|
|
528
|
+
<label style={{
|
|
529
|
+
"display": "block",
|
|
530
|
+
"marginBottom": "8px",
|
|
531
|
+
"color": "#374151",
|
|
532
|
+
"fontSize": "14px",
|
|
533
|
+
"fontWeight": "600"
|
|
534
|
+
}}>Confirm Password</label>
|
|
535
|
+
<input id="signup-password-confirm" type="password" required style={{
|
|
536
|
+
"width": "100%",
|
|
537
|
+
"padding": "12px 16px",
|
|
538
|
+
"border": "1px solid #E5E7EB",
|
|
539
|
+
"borderRadius": "8px",
|
|
540
|
+
"fontSize": "16px",
|
|
541
|
+
"background": "#FFFFFF",
|
|
542
|
+
"color": "#111827",
|
|
543
|
+
"outline": "none",
|
|
544
|
+
"boxSizing": "border-box"
|
|
545
|
+
}}/>
|
|
546
|
+
</div>
|
|
547
|
+
<button type="submit" style={{
|
|
548
|
+
"width": "100%",
|
|
549
|
+
"padding": "12px",
|
|
550
|
+
"background": "#7C3AED",
|
|
551
|
+
"color": "#FFFFFF",
|
|
552
|
+
"border": "1px solid #6D28D9",
|
|
553
|
+
"borderRadius": "8px",
|
|
554
|
+
"fontSize": "15px",
|
|
555
|
+
"fontWeight": "600",
|
|
556
|
+
"cursor": "pointer",
|
|
557
|
+
"boxShadow": "0 1px 2px rgba(124,58,237,0.3)",
|
|
558
|
+
"transition": "all 0.2s ease"
|
|
559
|
+
}}>Create Account</button>
|
|
560
|
+
</form>
|
|
561
|
+
<div style={{
|
|
562
|
+
"marginTop": "16px",
|
|
563
|
+
"textAlign": "center",
|
|
564
|
+
"background": "#F9FAFB",
|
|
565
|
+
"padding": "10px",
|
|
566
|
+
"borderRadius": "8px",
|
|
567
|
+
"border": "1px solid #E5E7EB"
|
|
568
|
+
}}>
|
|
569
|
+
<Link href="/login" style={{
|
|
570
|
+
"color": "#7C3AED",
|
|
571
|
+
"textDecoration": "none",
|
|
572
|
+
"fontWeight": "500"
|
|
573
|
+
}}>Already have an account? Login</Link>
|
|
574
|
+
</div>
|
|
575
|
+
</div>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async def handle_signup(e: any) -> None {
|
|
579
|
+
e.preventDefault();
|
|
580
|
+
username = document.getElementById("signup-username").value;
|
|
581
|
+
password = document.getElementById("signup-password").value;
|
|
582
|
+
confirm = document.getElementById("signup-password-confirm").value;
|
|
583
|
+
|
|
584
|
+
if password != confirm { alert("Passwords do not match"); return; }
|
|
585
|
+
if username.length < 3 { alert("Username must be at least 3 characters"); return; }
|
|
586
|
+
if password.length < 6 { alert("Password must be at least 6 characters"); return; }
|
|
587
|
+
|
|
588
|
+
result = await jacSignup(username, password);
|
|
589
|
+
|
|
590
|
+
if result["success"] if "success" in result else False {
|
|
591
|
+
alert("Account created successfully! Welcome to My Todo!");
|
|
592
|
+
navigate("/todos");
|
|
593
|
+
} else {
|
|
594
|
+
alert(result["error"] if "error" in result else "Signup failed");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
def logout_action() -> None {
|
|
599
|
+
jacLogout();
|
|
600
|
+
setTodoState({"items": [
|
|
601
|
+
{"id": 1, "text": "Sign up for an account", "done": False},
|
|
602
|
+
{"id": 2, "text": "Log in", "done": False},
|
|
603
|
+
{"id": 3, "text": "Add a new todo", "done": False},
|
|
604
|
+
{"id": 4, "text": "Toggle a todo", "done": False},
|
|
605
|
+
{"id": 5, "text": "Filter active/completed", "done": False},
|
|
606
|
+
{"id": 6, "text": "Clear completed", "done": False}
|
|
607
|
+
], "filter": "all", "input": ""});
|
|
608
|
+
navigate("/login");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# -----------------------------
|
|
612
|
+
# Shell + Router
|
|
613
|
+
# -----------------------------
|
|
614
|
+
def Nav(route: str) -> any {
|
|
615
|
+
if not jacIsLoggedIn() or route == "/login" or route == "/signup" { return None; }
|
|
616
|
+
return <nav style={{
|
|
617
|
+
"background": "#FFFFFF",
|
|
618
|
+
"padding": "12px",
|
|
619
|
+
"boxShadow": "0 1px 2px rgba(17,24,39,0.06)",
|
|
620
|
+
"border": "1px solid #E5E7EB",
|
|
621
|
+
"borderRadius": "10px"
|
|
622
|
+
}}>
|
|
623
|
+
<div style={{
|
|
624
|
+
"maxWidth": "960px",
|
|
625
|
+
"margin": "0 auto",
|
|
626
|
+
"display": "flex",
|
|
627
|
+
"gap": "16px",
|
|
628
|
+
"alignItems": "center",
|
|
629
|
+
"padding": "0 12px"
|
|
630
|
+
}}>
|
|
631
|
+
<Link href="/todos" style={{"textDecoration": "none"}}>
|
|
632
|
+
<span style={{
|
|
633
|
+
"color": "#111827",
|
|
634
|
+
"fontWeight": "800",
|
|
635
|
+
"fontSize": "18px"
|
|
636
|
+
}}>📝 My Todos</span>
|
|
637
|
+
</Link>
|
|
638
|
+
<button
|
|
639
|
+
onClick={logout_action}
|
|
640
|
+
style={{
|
|
641
|
+
"marginLeft": "auto",
|
|
642
|
+
"padding": "8px 12px",
|
|
643
|
+
"background": "#FFFFFF",
|
|
644
|
+
"color": "#374151",
|
|
645
|
+
"border": "1px solid #E5E7EB",
|
|
646
|
+
"borderRadius": "18px",
|
|
647
|
+
"cursor": "pointer",
|
|
648
|
+
"fontSize": "14px",
|
|
649
|
+
"fontWeight": "600",
|
|
650
|
+
"boxShadow": "none",
|
|
651
|
+
"transition": "all 0.2s ease"
|
|
652
|
+
}}
|
|
653
|
+
>
|
|
654
|
+
Logout
|
|
655
|
+
</button>
|
|
656
|
+
|
|
657
|
+
</div>
|
|
658
|
+
</nav>;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
def App() -> any {
|
|
662
|
+
login_route = {"path": "/login", "component": lambda -> any { return LoginForm(); }, "guard": None};
|
|
663
|
+
signup_route = {"path": "/signup", "component": lambda -> any { return SignupForm(); }, "guard": None};
|
|
664
|
+
todos_route = {"path": "/todos", "component": lambda -> any { return TodoApp(); }, "guard": jacIsLoggedIn};
|
|
665
|
+
|
|
666
|
+
routes = [login_route, signup_route, todos_route];
|
|
667
|
+
router = initRouter(routes, "/login"); # changed from createRouter
|
|
668
|
+
|
|
669
|
+
currentPath = router.path();
|
|
670
|
+
return <div style={{
|
|
671
|
+
"minHeight": "95vh",
|
|
672
|
+
"background": "#F7F8FA",
|
|
673
|
+
"padding": "24px"
|
|
674
|
+
}}>
|
|
675
|
+
{Nav(currentPath)}
|
|
676
|
+
<div style={{"maxWidth": "960px", "margin": "0 auto", "padding": "20px"}}>
|
|
677
|
+
{router.render()}
|
|
678
|
+
</div>
|
|
679
|
+
</div>;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
def jac_app() -> any { return App(); }
|
|
683
|
+
}
|