jac-client 0.2.6__py3-none-any.whl → 0.2.11__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.
Files changed (119) hide show
  1. jac_client/examples/all-in-one/{src/button.jac → button.jac} +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +47 -0
  3. jac_client/examples/all-in-one/components/Header.jac +17 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +64 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +76 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +188 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +62 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +44 -0
  9. jac_client/examples/all-in-one/components/button.jac +8 -0
  10. jac_client/examples/all-in-one/components/navigation.jac +126 -0
  11. jac_client/examples/all-in-one/constants/categories.jac +36 -0
  12. jac_client/examples/all-in-one/constants/clients.jac +12 -0
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +31 -0
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +122 -0
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +37 -0
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +140 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +157 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +124 -0
  20. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
  21. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
  22. jac_client/examples/all-in-one/pages/loginPage.jac +127 -0
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +54 -0
  24. jac_client/examples/all-in-one/pages/notFound.jac +18 -0
  25. jac_client/examples/all-in-one/pages/signupPage.jac +127 -0
  26. jac_client/examples/all-in-one/utils/formatters.jac +49 -0
  27. jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
  28. jac_client/examples/asset-serving/image-asset/main.jac +56 -0
  29. jac_client/examples/asset-serving/import-alias/main.jac +109 -0
  30. jac_client/examples/basic/main.jac +23 -0
  31. jac_client/examples/basic-auth/main.jac +363 -0
  32. jac_client/examples/basic-auth-with-router/main.jac +451 -0
  33. jac_client/examples/basic-full-stack/main.jac +362 -0
  34. jac_client/examples/css-styling/js-styling/main.jac +63 -0
  35. jac_client/examples/css-styling/material-ui/main.jac +122 -0
  36. jac_client/examples/css-styling/pure-css/main.jac +55 -0
  37. jac_client/examples/css-styling/sass-example/main.jac +55 -0
  38. jac_client/examples/css-styling/styled-components/main.jac +62 -0
  39. jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
  40. jac_client/examples/full-stack-with-auth/main.jac +696 -0
  41. jac_client/examples/little-x/main.jac +681 -0
  42. jac_client/examples/little-x/src/submit-button.jac +15 -14
  43. jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
  44. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
  45. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
  46. jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
  47. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
  48. jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
  49. jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
  50. jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
  51. jac_client/examples/ts-support/main.jac +35 -0
  52. jac_client/examples/with-router/main.jac +286 -0
  53. jac_client/plugin/cli.jac +507 -470
  54. jac_client/plugin/client.jac +30 -12
  55. jac_client/plugin/client_runtime.cl.jac +25 -15
  56. jac_client/plugin/impl/client.impl.jac +126 -26
  57. jac_client/plugin/impl/client_runtime.impl.jac +182 -10
  58. jac_client/plugin/plugin_config.jac +216 -34
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +2 -2
  61. jac_client/plugin/src/config_loader.jac +1 -0
  62. jac_client/plugin/src/desktop_config.jac +31 -0
  63. jac_client/plugin/src/impl/compiler.impl.jac +99 -30
  64. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  65. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  66. jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
  67. jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
  68. jac_client/plugin/src/impl/vite_bundler.impl.jac +384 -144
  69. jac_client/plugin/src/package_installer.jac +1 -1
  70. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  71. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  72. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  73. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  74. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  75. jac_client/plugin/src/targets/register.jac +21 -0
  76. jac_client/plugin/src/targets/registry.jac +87 -0
  77. jac_client/plugin/src/targets/web_target.jac +35 -0
  78. jac_client/plugin/src/vite_bundler.jac +15 -1
  79. jac_client/plugin/utils/__init__.jac +3 -0
  80. jac_client/plugin/utils/bun_installer.jac +16 -0
  81. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  82. jac_client/templates/client.jacpack +72 -0
  83. jac_client/templates/fullstack.jacpack +61 -0
  84. jac_client/tests/conftest.py +110 -52
  85. jac_client/tests/fixtures/spawn_test/app.jac +64 -70
  86. jac_client/tests/fixtures/with-ts/app.jac +28 -28
  87. jac_client/tests/test_cli.py +280 -113
  88. jac_client/tests/test_e2e.py +232 -0
  89. jac_client/tests/test_helpers.py +58 -0
  90. jac_client/tests/test_it.py +325 -154
  91. jac_client/tests/test_it_desktop.py +891 -0
  92. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/METADATA +20 -11
  93. jac_client-0.2.11.dist-info/RECORD +113 -0
  94. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  95. jac_client/examples/all-in-one/src/app.jac +0 -841
  96. jac_client/examples/all-in-one/src/components/button.jac +0 -7
  97. jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
  98. jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
  99. jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
  100. jac_client/examples/basic/src/app.jac +0 -21
  101. jac_client/examples/basic-auth/src/app.jac +0 -377
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -365
  104. jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
  105. jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
  106. jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
  107. jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
  108. jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
  109. jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
  110. jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
  111. jac_client/examples/little-x/src/app.jac +0 -719
  112. jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
  113. jac_client/examples/ts-support/src/app.jac +0 -35
  114. jac_client/examples/with-router/src/app.jac +0 -323
  115. jac_client/plugin/src/babel_processor.jac +0 -18
  116. jac_client/plugin/src/impl/babel_processor.impl.jac +0 -84
  117. jac_client-0.2.6.dist-info/RECORD +0 -74
  118. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,696 @@
1
+ # Full Stack Todo App with Auth and React Router
2
+ cl import from react {
3
+ useState,
4
+ useEffect
5
+ }
6
+ cl import from "@jac/runtime" {
7
+ Router,
8
+ Routes,
9
+ Route,
10
+ Link,
11
+ Navigate,
12
+ useNavigate,
13
+ useLocation,
14
+ jacSignup,
15
+ jacLogin,
16
+ jacLogout,
17
+ jacIsLoggedIn
18
+ }
19
+
20
+ # Backend - Todo Node
21
+ node Todo {
22
+ has text: str,
23
+ done: bool = False;
24
+ }
25
+
26
+ # Backend - Walkers
27
+ walker create_todo {
28
+ has text: str;
29
+
30
+ can create with `root entry {
31
+ new_todo = here ++> Todo(text=self.text);
32
+ report new_todo;
33
+ }
34
+ }
35
+
36
+ walker read_todos {
37
+ can read with `root entry {
38
+ visit [-->(`?Todo)];
39
+ }
40
+
41
+ can report_todos with Todo entry {
42
+ report here;
43
+ }
44
+ }
45
+
46
+ walker toggle_todo {
47
+ can toggle with Todo entry {
48
+ here.done = not here.done;
49
+ report here;
50
+ }
51
+ }
52
+
53
+ # Frontend Components
54
+ cl {
55
+ # Navigation
56
+ def Navigation -> any {
57
+ isLoggedIn = jacIsLoggedIn();
58
+ navigate = useNavigate();
59
+
60
+ def handleLogout(e: any) -> None {
61
+ e.preventDefault();
62
+ jacLogout();
63
+ navigate("/login");
64
+ }
65
+
66
+ if isLoggedIn {
67
+ return
68
+ <nav
69
+ style={{
70
+ "padding": "12px 24px",
71
+ "background": "#3b82f6",
72
+ "color": "#ffffff",
73
+ "display": "flex",
74
+ "justifyContent": "space-between"
75
+ }}
76
+ >
77
+ <div style={{"fontWeight": "600"}}>
78
+ Todo App
79
+ </div>
80
+ <div style={{"display": "flex", "gap": "16px"}}>
81
+ <Link
82
+ to="/todos"
83
+ style={{"color": "#ffffff", "textDecoration": "none"}}
84
+ >
85
+ Todos
86
+ </Link>
87
+ <button
88
+ onClick={handleLogout}
89
+ style={{
90
+ "background": "none",
91
+ "color": "#ffffff",
92
+ "border": "1px solid #ffffff",
93
+ "padding": "2px 10px",
94
+ "borderRadius": "4px",
95
+ "cursor": "pointer"
96
+ }}
97
+ >
98
+ Logout
99
+ </button>
100
+ </div>
101
+ </nav>;
102
+ }
103
+
104
+ return
105
+ <nav
106
+ style={{
107
+ "padding": "12px 24px",
108
+ "background": "#3b82f6",
109
+ "color": "#ffffff",
110
+ "display": "flex",
111
+ "justifyContent": "space-between"
112
+ }}
113
+ >
114
+ <div style={{"fontWeight": "600"}}>
115
+ Todo App
116
+ </div>
117
+ <div style={{"display": "flex", "gap": "16px"}}>
118
+ <Link
119
+ to="/login"
120
+ style={{"color": "#ffffff", "textDecoration": "none"}}
121
+ >
122
+ Login
123
+ </Link>
124
+ <Link
125
+ to="/signup"
126
+ style={{"color": "#ffffff", "textDecoration": "none"}}
127
+ >
128
+ Sign Up
129
+ </Link>
130
+ </div>
131
+ </nav>;
132
+ }
133
+
134
+ # Login Page
135
+ def LoginPage -> any {
136
+ has username: str = "",
137
+ password: str = "",
138
+ error: str = "";
139
+
140
+ navigate = useNavigate();
141
+
142
+ async def handleLogin(e: any) -> None {
143
+ e.preventDefault();
144
+ error = "";
145
+ if not username or not password {
146
+ error = "Please fill in all fields";
147
+ return;
148
+ }
149
+ success = await jacLogin(username, password);
150
+ if success {
151
+ navigate("/todos");
152
+ } else {
153
+ error = "Invalid credentials";
154
+ }
155
+ }
156
+
157
+ def handleUsernameChange(e: any) -> None {
158
+ username = e.target.value;
159
+ }
160
+
161
+ def handlePasswordChange(e: any) -> None {
162
+ password = e.target.value;
163
+ }
164
+
165
+ errorDisplay = None;
166
+ if error {
167
+ errorDisplay = <div
168
+ style={{"color": "#dc2626", "fontSize": "14px", "marginBottom": "10px"}}
169
+ >
170
+ {error}
171
+ </div>;
172
+ }
173
+
174
+ return
175
+ <div
176
+ style={{
177
+ "minHeight": "calc(100vh - 48px)",
178
+ "display": "flex",
179
+ "alignItems": "center",
180
+ "justifyContent": "center",
181
+ "background": "#f5f5f5"
182
+ }}
183
+ >
184
+ <div
185
+ style={{
186
+ "background": "#ffffff",
187
+ "padding": "30px",
188
+ "borderRadius": "8px",
189
+ "width": "280px",
190
+ "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
191
+ }}
192
+ >
193
+ <h2 style={{"marginBottom": "20px"}}>
194
+ Login
195
+ </h2>
196
+ <form onSubmit={handleLogin}>
197
+ <input
198
+ type="text"
199
+ value={username}
200
+ onChange={handleUsernameChange}
201
+ placeholder="Username"
202
+ style={{
203
+ "width": "100%",
204
+ "padding": "8px",
205
+ "marginBottom": "10px",
206
+ "border": "1px solid #ddd",
207
+ "borderRadius": "4px",
208
+ "boxSizing": "border-box"
209
+ }}
210
+ />
211
+ <input
212
+ type="password"
213
+ value={password}
214
+ onChange={handlePasswordChange}
215
+ placeholder="Password"
216
+ style={{
217
+ "width": "100%",
218
+ "padding": "8px",
219
+ "marginBottom": "10px",
220
+ "border": "1px solid #ddd",
221
+ "borderRadius": "4px",
222
+ "boxSizing": "border-box"
223
+ }}
224
+ />
225
+ {errorDisplay}
226
+ <button
227
+ type="submit"
228
+ style={{
229
+ "width": "100%",
230
+ "padding": "8px",
231
+ "background": "#3b82f6",
232
+ "color": "#ffffff",
233
+ "border": "none",
234
+ "borderRadius": "4px",
235
+ "cursor": "pointer",
236
+ "fontWeight": "600"
237
+ }}
238
+ >
239
+ Login
240
+ </button>
241
+ </form>
242
+ <p
243
+ style={{
244
+ "textAlign": "center",
245
+ "marginTop": "12px",
246
+ "fontSize": "14px"
247
+ }}
248
+ >
249
+ Need an account?
250
+ <Link to="/signup">
251
+ Sign up
252
+ </Link>
253
+ </p>
254
+ </div>
255
+ </div>;
256
+ }
257
+
258
+ # Signup Page
259
+ def SignupPage -> any {
260
+ has username: str = "",
261
+ password: str = "",
262
+ error: str = "";
263
+
264
+ navigate = useNavigate();
265
+
266
+ async def handleSignup(e: any) -> None {
267
+ e.preventDefault();
268
+ error = "";
269
+ if not username or not password {
270
+ error = "Please fill in all fields";
271
+ return;
272
+ }
273
+ result = await jacSignup(username, password);
274
+ if result["success"] {
275
+ navigate("/todos");
276
+ } else {
277
+ error = result["error"] if result["error"] else "Signup failed";
278
+ }
279
+ }
280
+
281
+ def handleUsernameChange(e: any) -> None {
282
+ username = e.target.value;
283
+ }
284
+
285
+ def handlePasswordChange(e: any) -> None {
286
+ password = e.target.value;
287
+ }
288
+
289
+ errorDisplay = None;
290
+ if error {
291
+ errorDisplay = <div
292
+ style={{"color": "#dc2626", "fontSize": "14px", "marginBottom": "10px"}}
293
+ >
294
+ {error}
295
+ </div>;
296
+ }
297
+
298
+ return
299
+ <div
300
+ style={{
301
+ "minHeight": "calc(100vh - 48px)",
302
+ "display": "flex",
303
+ "alignItems": "center",
304
+ "justifyContent": "center",
305
+ "background": "#f5f5f5"
306
+ }}
307
+ >
308
+ <div
309
+ style={{
310
+ "background": "#ffffff",
311
+ "padding": "30px",
312
+ "borderRadius": "8px",
313
+ "width": "280px",
314
+ "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
315
+ }}
316
+ >
317
+ <h2 style={{"marginBottom": "20px"}}>
318
+ Sign Up
319
+ </h2>
320
+ <form onSubmit={handleSignup}>
321
+ <input
322
+ type="text"
323
+ value={username}
324
+ onChange={handleUsernameChange}
325
+ placeholder="Username"
326
+ style={{
327
+ "width": "100%",
328
+ "padding": "8px",
329
+ "marginBottom": "10px",
330
+ "border": "1px solid #ddd",
331
+ "borderRadius": "4px",
332
+ "boxSizing": "border-box"
333
+ }}
334
+ />
335
+ <input
336
+ type="password"
337
+ value={password}
338
+ onChange={handlePasswordChange}
339
+ placeholder="Password"
340
+ style={{
341
+ "width": "100%",
342
+ "padding": "8px",
343
+ "marginBottom": "10px",
344
+ "border": "1px solid #ddd",
345
+ "borderRadius": "4px",
346
+ "boxSizing": "border-box"
347
+ }}
348
+ />
349
+ {errorDisplay}
350
+ <button
351
+ type="submit"
352
+ style={{
353
+ "width": "100%",
354
+ "padding": "8px",
355
+ "background": "#3b82f6",
356
+ "color": "#ffffff",
357
+ "border": "none",
358
+ "borderRadius": "4px",
359
+ "cursor": "pointer",
360
+ "fontWeight": "600"
361
+ }}
362
+ >
363
+ Sign Up
364
+ </button>
365
+ </form>
366
+ <p
367
+ style={{
368
+ "textAlign": "center",
369
+ "marginTop": "12px",
370
+ "fontSize": "14px"
371
+ }}
372
+ >
373
+ Have an account?
374
+ <Link to="/login">
375
+ Login
376
+ </Link>
377
+ </p>
378
+ </div>
379
+ </div>;
380
+ }
381
+
382
+ # TodoInput Component
383
+ def TodoInput(props: any) -> any {
384
+ input = props.input;
385
+ setInput = props.setInput;
386
+ addTodo = props.addTodo;
387
+
388
+ return
389
+ <div style={{"display": "flex", "gap": "8px", "marginBottom": "16px"}}>
390
+ <input
391
+ type="text"
392
+ value={input}
393
+ onChange={lambda e: any -> None { setInput(e.target.value);}}
394
+ onKeyPress={lambda e: any -> None { if e.key == "Enter" {
395
+ addTodo();
396
+ }}}
397
+ placeholder="What needs to be done?"
398
+ style={{
399
+ "flex": "1",
400
+ "padding": "8px",
401
+ "border": "1px solid #ddd",
402
+ "borderRadius": "4px"
403
+ }}
404
+ />
405
+ <button
406
+ onClick={addTodo}
407
+ style={{
408
+ "padding": "8px 16px",
409
+ "background": "#3b82f6",
410
+ "color": "#ffffff",
411
+ "border": "none",
412
+ "borderRadius": "4px",
413
+ "cursor": "pointer",
414
+ "fontWeight": "600"
415
+ }}
416
+ >
417
+ Add
418
+ </button>
419
+ </div>;
420
+ }
421
+
422
+ # TodoFilters Component
423
+ def TodoFilters(props: any) -> any {
424
+ filter = props.filter;
425
+ setFilter = props.setFilter;
426
+
427
+ return
428
+ <div style={{"display": "flex", "gap": "8px", "marginBottom": "16px"}}>
429
+ <button
430
+ onClick={lambda -> None { setFilter("all");}}
431
+ style={{
432
+ "padding": "6px 12px",
433
+ "background": ("#3b82f6" if filter == "all" else "#e5e7eb"),
434
+ "color": ("#ffffff" if filter == "all" else "#000000"),
435
+ "border": "none",
436
+ "borderRadius": "4px",
437
+ "cursor": "pointer",
438
+ "fontSize": "14px"
439
+ }}
440
+ >
441
+ All
442
+ </button>
443
+ <button
444
+ onClick={lambda -> None { setFilter("active");}}
445
+ style={{
446
+ "padding": "6px 12px",
447
+ "background": ("#3b82f6" if filter == "active" else "#e5e7eb"),
448
+ "color": ("#ffffff" if filter == "active" else "#000000"),
449
+ "border": "none",
450
+ "borderRadius": "4px",
451
+ "cursor": "pointer",
452
+ "fontSize": "14px"
453
+ }}
454
+ >
455
+ Active
456
+ </button>
457
+ <button
458
+ onClick={lambda -> None { setFilter("completed");}}
459
+ style={{
460
+ "padding": "6px 12px",
461
+ "background": (
462
+ "#3b82f6" if filter == "completed" else "#e5e7eb"
463
+ ),
464
+ "color": ("#ffffff" if filter == "completed" else "#000000"),
465
+ "border": "none",
466
+ "borderRadius": "4px",
467
+ "cursor": "pointer",
468
+ "fontSize": "14px"
469
+ }}
470
+ >
471
+ Completed
472
+ </button>
473
+ </div>;
474
+ }
475
+
476
+ # TodoItem Component
477
+ def TodoItem(props: any) -> any {
478
+ todo = props.todo;
479
+ toggleTodo = props.toggleTodo;
480
+ deleteTodo = props.deleteTodo;
481
+
482
+ return
483
+ <div
484
+ key={todo._jac_id}
485
+ style={{
486
+ "display": "flex",
487
+ "alignItems": "center",
488
+ "gap": "10px",
489
+ "padding": "10px",
490
+ "borderBottom": "1px solid #e5e7eb"
491
+ }}
492
+ >
493
+ <input
494
+ type="checkbox"
495
+ checked={todo.done}
496
+ onChange={lambda -> None { toggleTodo(todo._jac_id);}}
497
+ style={{"cursor": "pointer"}}
498
+ />
499
+ <span
500
+ style={{
501
+ "flex": "1",
502
+ "textDecoration": ("line-through" if todo.done else "none"),
503
+ "color": ("#999" if todo.done else "#000")
504
+ }}
505
+ >
506
+ {todo.text}
507
+ </span>
508
+ <button
509
+ onClick={lambda -> None { deleteTodo(todo._jac_id);}}
510
+ style={{
511
+ "padding": "4px 8px",
512
+ "background": "#ef4444",
513
+ "color": "#ffffff",
514
+ "border": "none",
515
+ "borderRadius": "4px",
516
+ "cursor": "pointer",
517
+ "fontSize": "12px"
518
+ }}
519
+ >
520
+ Delete
521
+ </button>
522
+ </div>;
523
+ }
524
+
525
+ # TodoList Component
526
+ def TodoList(props: any) -> any {
527
+ filteredTodos = props.filteredTodos;
528
+ toggleTodo = props.toggleTodo;
529
+ deleteTodo = props.deleteTodo;
530
+
531
+ if filteredTodos.length == 0 {
532
+ return
533
+ <div
534
+ style={{"padding": "20px", "textAlign": "center", "color": "#999"}}
535
+ >
536
+ No todos yet. Add one above!
537
+ </div>;
538
+ }
539
+
540
+ return
541
+ <div>
542
+ {filteredTodos.map(
543
+ lambda todo: any -> any { return
544
+ <TodoItem
545
+ key={todo._jac_id}
546
+ todo={todo}
547
+ toggleTodo={toggleTodo}
548
+ deleteTodo={deleteTodo}
549
+ />; }
550
+ )}
551
+ </div>;
552
+ }
553
+
554
+ # Todos Page (Protected)
555
+ def TodosPage -> any {
556
+ # Check if user is logged in, redirect if not
557
+ if not jacIsLoggedIn() {
558
+ return
559
+ <Navigate to="/login" />;
560
+ }
561
+
562
+ [todos, setTodos] = useState([]);
563
+ [input, setInput] = useState("");
564
+ [filter, setFilter] = useState("all");
565
+
566
+ # Load todos on mount
567
+ useEffect(
568
+ lambda -> None { async def loadTodos -> None {
569
+ result = root spawn read_todos();
570
+ setTodos(result.reports or []);
571
+ } loadTodos();},
572
+ []
573
+ );
574
+
575
+ # Add todo
576
+ async def addTodo -> None {
577
+ if not input.trim() {
578
+ return;
579
+ }
580
+ result = root spawn create_todo(text=input.trim());
581
+ setTodos(todos.concat([result.reports[0][0]]));
582
+ setInput("");
583
+ }
584
+
585
+ # Toggle todo
586
+ async def toggleTodo(id: any) -> None {
587
+ id spawn toggle_todo();
588
+ setTodos(
589
+ todos.map(
590
+ lambda todo: any -> any { if todo._jac_id == id {
591
+ return {
592
+ "_jac_id": todo._jac_id,
593
+ "text": todo.text,
594
+ "done": not todo.done
595
+ };
596
+ }return todo; }
597
+ )
598
+ );
599
+ }
600
+
601
+ # Delete todo
602
+ async def deleteTodo(id: any) -> None {
603
+ #id spawn delete_todo();
604
+ setTodos(
605
+ todos.filter(lambda todo: any -> bool { return todo._jac_id != id; })
606
+ );
607
+ }
608
+
609
+ # Filter todos
610
+ def getFilteredTodos -> list {
611
+ if filter == "active" {
612
+ return todos.filter(
613
+ lambda todo: any -> bool { return not todo.done; }
614
+ );
615
+ } elif filter == "completed" {
616
+ return todos.filter(lambda todo: any -> bool { return todo.done; });
617
+ }
618
+ return todos;
619
+ }
620
+
621
+ filteredTodos = getFilteredTodos();
622
+ activeCount = todos.filter(lambda todo: any -> bool { return not todo.done; }).length;
623
+
624
+ return
625
+ <div
626
+ style={{
627
+ "maxWidth": "600px",
628
+ "margin": "20px auto",
629
+ "padding": "20px",
630
+ "background": "#ffffff",
631
+ "borderRadius": "8px",
632
+ "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
633
+ }}
634
+ >
635
+ <h1 style={{"marginBottom": "20px"}}>
636
+ My Todos
637
+ </h1>
638
+ <TodoInput input={input} setInput={setInput} addTodo={addTodo} />
639
+ <TodoFilters filter={filter} setFilter={setFilter} />
640
+ <TodoList
641
+ filteredTodos={filteredTodos}
642
+ toggleTodo={toggleTodo}
643
+ deleteTodo={deleteTodo}
644
+ />
645
+ {(
646
+ <div
647
+ style={{
648
+ "marginTop": "16px",
649
+ "padding": "10px",
650
+ "background": "#f9fafb",
651
+ "borderRadius": "4px",
652
+ "fontSize": "14px",
653
+ "color": "#666"
654
+ }}
655
+ >
656
+ {activeCount}{"item" if activeCount == 1 else "items"}left
657
+ </div>
658
+ )
659
+ if todos.length > 0
660
+ else None}
661
+ </div>;
662
+ }
663
+
664
+ # Home/Landing Page - auto-redirect
665
+ def HomePage -> any {
666
+ if jacIsLoggedIn() {
667
+ return
668
+ <Navigate to="/todos" />;
669
+ }
670
+ return
671
+ <Navigate to="/login" />;
672
+ }
673
+
674
+ # Main App with React Router
675
+ def:pub app -> any {
676
+ return
677
+ <Router>
678
+ <div style={{"fontFamily": "system-ui, sans-serif"}}>
679
+ <Navigation />
680
+ <Routes>
681
+ <Route path="/" element={<HomePage />} />
682
+ <Route path="/login" element={<LoginPage />} />
683
+ <Route path="/signup" element={<SignupPage />} />
684
+ <Route path="/todos" element={<TodosPage />} />
685
+ </Routes>
686
+ </div>
687
+ </Router>;
688
+ }
689
+ }
690
+ # Add todo input
691
+
692
+ # Filter buttons
693
+
694
+ # Todo list
695
+
696
+ # Stats