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