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.
- jac_client/docs/README.md +659 -0
- jac_client/docs/advanced-state.md +1266 -0
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/guide-example/intro.md +117 -0
- jac_client/docs/guide-example/step-01-setup.md +260 -0
- jac_client/docs/guide-example/step-02-components.md +416 -0
- jac_client/docs/guide-example/step-03-styling.md +478 -0
- jac_client/docs/guide-example/step-04-todo-ui.md +477 -0
- jac_client/docs/guide-example/step-05-local-state.md +530 -0
- jac_client/docs/guide-example/step-06-events.md +750 -0
- jac_client/docs/guide-example/step-07-effects.md +469 -0
- jac_client/docs/guide-example/step-08-walkers.md +534 -0
- jac_client/docs/guide-example/step-09-authentication.md +586 -0
- jac_client/docs/guide-example/step-10-routing.md +540 -0
- jac_client/docs/guide-example/step-11-final.md +964 -0
- jac_client/docs/imports.md +1142 -0
- jac_client/docs/lifecycle-hooks.md +774 -0
- jac_client/docs/routing.md +660 -0
- jac_client/examples/basic/.babelrc +9 -0
- jac_client/examples/basic/README.md +16 -0
- jac_client/examples/basic/app.jac +16 -0
- jac_client/examples/basic/package.json +27 -0
- jac_client/examples/basic/vite.config.js +28 -0
- jac_client/examples/basic-auth/.babelrc +9 -0
- jac_client/examples/basic-auth/README.md +16 -0
- jac_client/examples/basic-auth/app.jac +308 -0
- jac_client/examples/basic-auth/package.json +27 -0
- jac_client/examples/basic-auth/vite.config.js +28 -0
- jac_client/examples/basic-auth-with-router/.babelrc +9 -0
- jac_client/examples/basic-auth-with-router/README.md +60 -0
- jac_client/examples/basic-auth-with-router/app.jac +464 -0
- jac_client/examples/basic-auth-with-router/package.json +28 -0
- jac_client/examples/basic-auth-with-router/vite.config.js +28 -0
- jac_client/examples/basic-full-stack/.babelrc +9 -0
- jac_client/examples/basic-full-stack/README.md +18 -0
- jac_client/examples/basic-full-stack/app.jac +320 -0
- jac_client/examples/basic-full-stack/package.json +28 -0
- jac_client/examples/basic-full-stack/vite.config.js +28 -0
- jac_client/examples/full-stack-with-auth/.babelrc +9 -0
- jac_client/examples/full-stack-with-auth/README.md +16 -0
- jac_client/examples/full-stack-with-auth/app.jac +735 -0
- jac_client/examples/full-stack-with-auth/package.json +28 -0
- jac_client/examples/full-stack-with-auth/vite.config.js +30 -0
- jac_client/examples/little-x/app.jac +615 -0
- jac_client/examples/little-x/package.json +23 -0
- jac_client/examples/little-x/submit-button.jac +8 -0
- jac_client/examples/with-router/.babelrc +9 -0
- jac_client/examples/with-router/README.md +17 -0
- jac_client/examples/with-router/app.jac +323 -0
- jac_client/examples/with-router/package.json +28 -0
- jac_client/examples/with-router/vite.config.js +28 -0
- jac_client/plugin/cli.py +239 -0
- jac_client/plugin/client.py +89 -0
- jac_client/plugin/client_runtime.jac +234 -0
- jac_client/plugin/vite_client_bundle.py +355 -0
- jac_client/tests/__init__.py +2 -0
- jac_client/tests/fixtures/basic-app/app.jac +18 -0
- jac_client/tests/fixtures/client_app_with_antd/app.jac +28 -0
- jac_client/tests/fixtures/js_import/app.jac +30 -0
- jac_client/tests/fixtures/js_import/utils.js +22 -0
- jac_client/tests/fixtures/package-lock.json +329 -0
- jac_client/tests/fixtures/package.json +11 -0
- jac_client/tests/fixtures/relative_import/app.jac +13 -0
- jac_client/tests/fixtures/relative_import/button.jac +6 -0
- jac_client/tests/fixtures/spawn_test/app.jac +133 -0
- jac_client/tests/fixtures/test_fragments_spread/app.jac +53 -0
- jac_client/tests/test_cl.py +476 -0
- jac_client/tests/test_create_jac_app.py +139 -0
- jac_client-0.2.0.dist-info/METADATA +182 -0
- jac_client-0.2.0.dist-info/RECORD +72 -0
- jac_client-0.2.0.dist-info/WHEEL +4 -0
- 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
|