jac-coder 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_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Core Jac Patterns — Complete Working Examples
|
|
2
|
+
|
|
3
|
+
## Basic Module with Entry Block
|
|
4
|
+
|
|
5
|
+
```jac
|
|
6
|
+
"""A simple greeting module."""
|
|
7
|
+
|
|
8
|
+
def greet(name: str) -> str {
|
|
9
|
+
return f"Hello, {name}!";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
with entry {
|
|
13
|
+
message = greet("World");
|
|
14
|
+
print(message);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Object with Fields and Methods
|
|
19
|
+
|
|
20
|
+
```jac
|
|
21
|
+
"""Object archetype with has declarations and methods."""
|
|
22
|
+
|
|
23
|
+
obj Person {
|
|
24
|
+
has name: str = "",
|
|
25
|
+
age: int = 0,
|
|
26
|
+
email: str = "";
|
|
27
|
+
|
|
28
|
+
def greet() -> str {
|
|
29
|
+
return f"Hi, I'm {self.name}, age {self.age}";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def is_adult() -> bool {
|
|
33
|
+
return self.age >= 18;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with entry {
|
|
38
|
+
p = Person(name="Alice", age=30, email="alice@example.com");
|
|
39
|
+
print(p.greet());
|
|
40
|
+
print(f"Adult: {p.is_adult()}");
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Interface and Implementation Separation
|
|
45
|
+
|
|
46
|
+
Declaration (`calculator.jac`):
|
|
47
|
+
|
|
48
|
+
```jac
|
|
49
|
+
obj Calculator {
|
|
50
|
+
has result: float = 0.0;
|
|
51
|
+
|
|
52
|
+
def add(x: float) -> float;
|
|
53
|
+
def subtract(x: float) -> float;
|
|
54
|
+
def reset() -> None;
|
|
55
|
+
def get_result() -> float;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Implementation (`impl/calculator.impl.jac`):
|
|
60
|
+
|
|
61
|
+
```jac
|
|
62
|
+
impl Calculator.add(x: float) -> float {
|
|
63
|
+
self.result += x;
|
|
64
|
+
return self.result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl Calculator.subtract(x: float) -> float {
|
|
68
|
+
self.result -= x;
|
|
69
|
+
return self.result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Calculator.reset() -> None {
|
|
73
|
+
self.result = 0.0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl Calculator.get_result() -> float {
|
|
77
|
+
return self.result;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Walker Traversal
|
|
82
|
+
|
|
83
|
+
```jac
|
|
84
|
+
"""Walker that traverses a city graph."""
|
|
85
|
+
|
|
86
|
+
node City {
|
|
87
|
+
has name: str = "",
|
|
88
|
+
population: int = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
edge Road {
|
|
92
|
+
has distance: float = 0.0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
walker Explorer {
|
|
96
|
+
has visited: list[str] = [];
|
|
97
|
+
|
|
98
|
+
can visit_city with City entry {
|
|
99
|
+
self.visited.append(here.name);
|
|
100
|
+
print(f"Visiting {here.name} (pop: {here.population})");
|
|
101
|
+
visit [-->];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
with entry {
|
|
106
|
+
nyc = City(name="New York", population=8_300_000);
|
|
107
|
+
la = City(name="Los Angeles", population=3_900_000);
|
|
108
|
+
chi = City(name="Chicago", population=2_700_000);
|
|
109
|
+
|
|
110
|
+
root ++> nyc;
|
|
111
|
+
nyc +>:Road(distance=790.0):+> chi;
|
|
112
|
+
chi +>:Road(distance=2015.0):+> la;
|
|
113
|
+
|
|
114
|
+
explorer = root spawn Explorer();
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Social Network Graph
|
|
119
|
+
|
|
120
|
+
```jac
|
|
121
|
+
"""Graph-based social network with typed edges."""
|
|
122
|
+
|
|
123
|
+
node User {
|
|
124
|
+
has username: str = "",
|
|
125
|
+
bio: str = "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
edge Follows {
|
|
129
|
+
has since: str = "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
with entry {
|
|
133
|
+
alice = User(username="alice", bio="Developer");
|
|
134
|
+
bob = User(username="bob", bio="Designer");
|
|
135
|
+
carol = User(username="carol", bio="Manager");
|
|
136
|
+
|
|
137
|
+
alice +>:Follows(since="2024-01"):+> bob;
|
|
138
|
+
alice +>:Follows(since="2024-03"):+> carol;
|
|
139
|
+
bob +>:Follows(since="2024-02"):+> carol;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## AI-Powered Function (by llm)
|
|
144
|
+
|
|
145
|
+
```jac
|
|
146
|
+
"""Using by llm() for AI-powered functions."""
|
|
147
|
+
|
|
148
|
+
import from byllm.lib { Model }
|
|
149
|
+
|
|
150
|
+
glob model: Model = Model(model_name="openai/gpt-4o-mini");
|
|
151
|
+
|
|
152
|
+
def summarize(text: str) -> str by model(
|
|
153
|
+
reason="Summarize the given text in 2-3 sentences"
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
enum Sentiment { POSITIVE, NEGATIVE, NEUTRAL }
|
|
157
|
+
|
|
158
|
+
def classify_sentiment(text: str) -> Sentiment by model(
|
|
159
|
+
reason="Classify the sentiment of the text"
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
with entry {
|
|
163
|
+
result = summarize("Long article text here...");
|
|
164
|
+
print(result);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Test Blocks
|
|
169
|
+
|
|
170
|
+
```jac
|
|
171
|
+
"""Tests for the calculator module."""
|
|
172
|
+
|
|
173
|
+
import from calculator { Calculator }
|
|
174
|
+
|
|
175
|
+
test "calculator addition" {
|
|
176
|
+
calc = Calculator();
|
|
177
|
+
calc.add(5.0);
|
|
178
|
+
assert calc.get_result() == 5.0;
|
|
179
|
+
calc.add(3.0);
|
|
180
|
+
assert calc.get_result() == 8.0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test "calculator reset" {
|
|
184
|
+
calc = Calculator();
|
|
185
|
+
calc.add(10.0);
|
|
186
|
+
calc.reset();
|
|
187
|
+
assert calc.get_result() == 0.0;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Enum and Match/Case
|
|
192
|
+
|
|
193
|
+
```jac
|
|
194
|
+
"""Enum types and pattern matching."""
|
|
195
|
+
|
|
196
|
+
enum Color {
|
|
197
|
+
RED = "red",
|
|
198
|
+
GREEN = "green",
|
|
199
|
+
BLUE = "blue"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def describe_value(x: object) -> str {
|
|
203
|
+
match x {
|
|
204
|
+
case int():
|
|
205
|
+
return f"Integer: {x}";
|
|
206
|
+
case str():
|
|
207
|
+
return f"String: {x}";
|
|
208
|
+
case list():
|
|
209
|
+
return f"List with {len(x)} items";
|
|
210
|
+
case _:
|
|
211
|
+
return "Unknown type";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
with entry {
|
|
216
|
+
print(describe_value(42));
|
|
217
|
+
print(describe_value("hello"));
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Async/Await
|
|
222
|
+
|
|
223
|
+
```jac
|
|
224
|
+
"""Async operations in Jac."""
|
|
225
|
+
|
|
226
|
+
import asyncio;
|
|
227
|
+
|
|
228
|
+
async def fetch_data(url: str) -> str {
|
|
229
|
+
await asyncio.sleep(0.1);
|
|
230
|
+
return f"Data from {url}";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
with entry {
|
|
234
|
+
result = asyncio.run(fetch_data("https://example.com"));
|
|
235
|
+
print(result);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## CRUD Backend with Graph
|
|
240
|
+
|
|
241
|
+
```jac
|
|
242
|
+
"""Complete CRUD backend using nodes and def:pub endpoints."""
|
|
243
|
+
|
|
244
|
+
import from uuid { uuid4 }
|
|
245
|
+
|
|
246
|
+
node Product {
|
|
247
|
+
has id: str = "";
|
|
248
|
+
has name: str = "";
|
|
249
|
+
has price: float = 0.0;
|
|
250
|
+
has category: str = "";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def:pub add_product(name: str, price: float, category: str = "") -> dict {
|
|
254
|
+
product = (root ++> Product(
|
|
255
|
+
id=str(uuid4()), name=name, price=price, category=category
|
|
256
|
+
))[0];
|
|
257
|
+
return {"id": product.id, "name": product.name, "price": product.price};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def:pub get_products(category: str = "") -> list {
|
|
261
|
+
all_products = [root-->][?:Product];
|
|
262
|
+
if category {
|
|
263
|
+
all_products = [p for p in all_products if p.category == category];
|
|
264
|
+
}
|
|
265
|
+
return [{"id": p.id, "name": p.name, "price": p.price}
|
|
266
|
+
for p in all_products];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def:pub delete_product(product_id: str) -> dict {
|
|
270
|
+
for p in [root-->][?:Product] {
|
|
271
|
+
if p.id == product_id {
|
|
272
|
+
root del--> p;
|
|
273
|
+
return {"success": True};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return {"success": False, "error": "Not found"};
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Multi-Node Walker Endpoint
|
|
281
|
+
|
|
282
|
+
```jac
|
|
283
|
+
"""Walker that traverses Order → OrderItems in one request."""
|
|
284
|
+
|
|
285
|
+
node Order {
|
|
286
|
+
has id: str = "";
|
|
287
|
+
has status: str = "pending";
|
|
288
|
+
has total: float = 0.0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
node OrderItem {
|
|
292
|
+
has name: str = "";
|
|
293
|
+
has qty: int = 0;
|
|
294
|
+
has price: float = 0.0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
walker :pub get_order_details {
|
|
298
|
+
has order_id: str = "";
|
|
299
|
+
|
|
300
|
+
can find_order with Root entry {
|
|
301
|
+
for order in [-->][?:Order] {
|
|
302
|
+
if order.id == self.order_id {
|
|
303
|
+
visit order;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
report {"error": "Order not found"};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
can collect_items with Order entry {
|
|
311
|
+
items = [{"name": i.name, "qty": i.qty, "price": i.price}
|
|
312
|
+
for i in [-->][?:OrderItem]];
|
|
313
|
+
report {
|
|
314
|
+
"order_id": here.id,
|
|
315
|
+
"status": here.status,
|
|
316
|
+
"items": items,
|
|
317
|
+
"total": here.total
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Example: Fullstack Todo App
|
|
2
|
+
|
|
3
|
+
Complete fullstack app: backend nodes, def:priv endpoints, hook, components, auth, file-based routing.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
todo-app/
|
|
9
|
+
├── jac.toml
|
|
10
|
+
├── main.jac # ALL backend + cl { app() }
|
|
11
|
+
├── components/
|
|
12
|
+
│ ├── Layout.cl.jac # Root layout
|
|
13
|
+
│ ├── Header.cl.jac # Nav bar
|
|
14
|
+
│ ├── TodoList.cl.jac # List + add form (calls hook)
|
|
15
|
+
│ ├── TodoItem.cl.jac # Single item display
|
|
16
|
+
│ └── AddTodoForm.cl.jac # Input form
|
|
17
|
+
├── hooks/
|
|
18
|
+
│ └── useTodos.cl.jac # Data hook (sv import + state)
|
|
19
|
+
├── pages/
|
|
20
|
+
│ ├── layout.jac # Root layout with nav
|
|
21
|
+
│ ├── (public)/login.jac # Login page
|
|
22
|
+
│ └── (auth)/
|
|
23
|
+
│ ├── layout.jac # Auth guard
|
|
24
|
+
│ └── dashboard.jac # Dashboard
|
|
25
|
+
└── styles/global.css
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## main.jac — Backend + Entry
|
|
29
|
+
|
|
30
|
+
```jac
|
|
31
|
+
import from uuid { uuid4 }
|
|
32
|
+
import from datetime { datetime }
|
|
33
|
+
|
|
34
|
+
node Todo {
|
|
35
|
+
has id: str = "";
|
|
36
|
+
has title: str = "";
|
|
37
|
+
has completed: bool = False;
|
|
38
|
+
has created_at: str = "";
|
|
39
|
+
|
|
40
|
+
def postinit {
|
|
41
|
+
if not self.id { self.id = str(uuid4()); }
|
|
42
|
+
if not self.created_at { self.created_at = datetime.now().isoformat(); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def:priv get_todos() -> list {
|
|
47
|
+
return [{"id": t.id, "title": t.title, "completed": t.completed}
|
|
48
|
+
for t in [root-->][?:Todo]];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def:priv add_todo(title: str) -> dict {
|
|
52
|
+
todo = (root ++> Todo(title=title))[0];
|
|
53
|
+
return {"id": todo.id, "title": todo.title, "completed": False};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def:priv toggle_todo(id: str) -> dict {
|
|
57
|
+
for todo in [root-->][?:Todo] {
|
|
58
|
+
if todo.id == id {
|
|
59
|
+
todo.completed = not todo.completed;
|
|
60
|
+
return {"id": todo.id, "completed": todo.completed};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {"error": "not found"};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def:priv delete_todo(id: str) -> dict {
|
|
67
|
+
for todo in [root-->][?:Todo] {
|
|
68
|
+
if todo.id == id {
|
|
69
|
+
root del--> todo;
|
|
70
|
+
return {"deleted": id};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {"error": "not found"};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cl import from .components.Layout { Layout }
|
|
77
|
+
cl {
|
|
78
|
+
def:pub app() -> JsxElement { return <Layout />; }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## hooks/useTodos.cl.jac — Data Hook
|
|
83
|
+
|
|
84
|
+
```jac
|
|
85
|
+
sv import from ..main { get_todos, add_todo, toggle_todo, delete_todo }
|
|
86
|
+
|
|
87
|
+
def:pub useTodos() -> dict {
|
|
88
|
+
has todos: list = [];
|
|
89
|
+
has loading: bool = True;
|
|
90
|
+
has error: str = "";
|
|
91
|
+
|
|
92
|
+
async can with entry {
|
|
93
|
+
try {
|
|
94
|
+
result = await get_todos();
|
|
95
|
+
todos = result or [];
|
|
96
|
+
} except Exception as e {
|
|
97
|
+
error = str(e);
|
|
98
|
+
}
|
|
99
|
+
loading = False;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async def handleAdd(title: str) -> None {
|
|
103
|
+
if not title { return; }
|
|
104
|
+
result = await add_todo(title);
|
|
105
|
+
if result and not result.error {
|
|
106
|
+
todos = todos + [result];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async def handleToggle(id: str) -> None {
|
|
111
|
+
result = await toggle_todo(id);
|
|
112
|
+
if result and not result.error {
|
|
113
|
+
todos = [
|
|
114
|
+
({"id": t["id"], "title": t["title"],
|
|
115
|
+
"completed": result["completed"]}
|
|
116
|
+
if t["id"] == id else t)
|
|
117
|
+
for t in todos
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async def handleDelete(id: str) -> None {
|
|
123
|
+
result = await delete_todo(id);
|
|
124
|
+
if result and not result.error {
|
|
125
|
+
todos = [t for t in todos if t["id"] != id];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"todos": todos, "loading": loading, "error": error,
|
|
131
|
+
"handleAdd": handleAdd, "handleToggle": handleToggle,
|
|
132
|
+
"handleDelete": handleDelete
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## components/Layout.cl.jac — Root Layout
|
|
138
|
+
|
|
139
|
+
```jac
|
|
140
|
+
import "..styles.global.css";
|
|
141
|
+
import from ".Header" { Header }
|
|
142
|
+
import from ".TodoList" { TodoList }
|
|
143
|
+
|
|
144
|
+
def:pub Layout() -> JsxElement {
|
|
145
|
+
return (
|
|
146
|
+
<div className="min-h-screen bg-gray-50">
|
|
147
|
+
<Header title="Todo App" />
|
|
148
|
+
<main className="max-w-2xl mx-auto px-4 py-8">
|
|
149
|
+
<TodoList />
|
|
150
|
+
</main>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## components/Header.cl.jac
|
|
157
|
+
|
|
158
|
+
```jac
|
|
159
|
+
def:pub Header(props: dict) -> JsxElement {
|
|
160
|
+
title = props.title or "";
|
|
161
|
+
return (
|
|
162
|
+
<header className="border-b px-6 py-4">
|
|
163
|
+
<h1 className="text-xl font-bold">{title}</h1>
|
|
164
|
+
</header>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## components/TodoItem.cl.jac
|
|
170
|
+
|
|
171
|
+
```jac
|
|
172
|
+
def:pub TodoItem(props: dict) -> JsxElement {
|
|
173
|
+
todo = props.todo or {};
|
|
174
|
+
onToggle = props.onToggle or None;
|
|
175
|
+
onDelete = props.onDelete or None;
|
|
176
|
+
todoId = todo["id"] or "";
|
|
177
|
+
title = todo["title"] or "";
|
|
178
|
+
completed = todo["completed"] or False;
|
|
179
|
+
|
|
180
|
+
def handle_toggle() -> None {
|
|
181
|
+
if onToggle { onToggle(todoId); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def handle_delete() -> None {
|
|
185
|
+
if onDelete { onDelete(todoId); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
|
190
|
+
<button onClick={handle_toggle} className="w-5 h-5 rounded border-2">
|
|
191
|
+
{completed and "✓"}
|
|
192
|
+
</button>
|
|
193
|
+
<span class={completed and "flex-1 line-through text-gray-400" or "flex-1"}>
|
|
194
|
+
{title}
|
|
195
|
+
</span>
|
|
196
|
+
<button onClick={handle_delete} className="text-red-500">×</button>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## components/AddTodoForm.cl.jac
|
|
203
|
+
|
|
204
|
+
```jac
|
|
205
|
+
def:pub AddTodoForm(props: dict) -> JsxElement {
|
|
206
|
+
onAdd = props.onAdd or None;
|
|
207
|
+
has inputValue: str = "";
|
|
208
|
+
|
|
209
|
+
def handleSubmit(e: any) -> None {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
if inputValue and onAdd {
|
|
212
|
+
onAdd(inputValue);
|
|
213
|
+
inputValue = "";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def handle_input(e: any) -> None {
|
|
218
|
+
inputValue = e.target.value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
223
|
+
<input value={inputValue} onChange={handle_input}
|
|
224
|
+
placeholder="What needs to be done?"
|
|
225
|
+
className="flex-1 px-4 py-2 border rounded-lg" />
|
|
226
|
+
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">+</button>
|
|
227
|
+
</form>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## components/TodoList.cl.jac
|
|
233
|
+
|
|
234
|
+
```jac
|
|
235
|
+
import from ".TodoItem" { TodoItem }
|
|
236
|
+
import from ".AddTodoForm" { AddTodoForm }
|
|
237
|
+
import from "..hooks.useTodos" { useTodos }
|
|
238
|
+
|
|
239
|
+
def:pub TodoList() -> JsxElement {
|
|
240
|
+
todoData = useTodos();
|
|
241
|
+
todos = todoData["todos"] or [];
|
|
242
|
+
|
|
243
|
+
if todoData["loading"] {
|
|
244
|
+
return <p className="text-gray-400 text-center py-8">Loading...</p>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
{todoData["error"] and (<p className="text-red-500 mb-4">{todoData["error"]}</p>)}
|
|
250
|
+
<div className="mb-6"><AddTodoForm onAdd={todoData["handleAdd"]} /></div>
|
|
251
|
+
{len(todos) == 0 and (<p className="text-gray-400 text-center py-8">No todos yet.</p>)}
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
{[
|
|
254
|
+
<TodoItem key={todo["id"]} todo={todo}
|
|
255
|
+
onToggle={todoData["handleToggle"]}
|
|
256
|
+
onDelete={todoData["handleDelete"]} />
|
|
257
|
+
for todo in todos
|
|
258
|
+
]}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## pages/(public)/login.jac — Login Page
|
|
266
|
+
|
|
267
|
+
```jac
|
|
268
|
+
cl import from "@jac/runtime" { jacLogin, useNavigate }
|
|
269
|
+
|
|
270
|
+
cl {
|
|
271
|
+
def:pub page() -> JsxElement {
|
|
272
|
+
has username: str = "";
|
|
273
|
+
has password: str = "";
|
|
274
|
+
has error: str = "";
|
|
275
|
+
has loading: bool = False;
|
|
276
|
+
navigate = useNavigate();
|
|
277
|
+
|
|
278
|
+
async def handleLogin(e: any) -> None {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
loading = True;
|
|
281
|
+
error = "";
|
|
282
|
+
success = await jacLogin(username, password);
|
|
283
|
+
if success { navigate("/dashboard"); }
|
|
284
|
+
else { error = "Invalid credentials"; }
|
|
285
|
+
loading = False;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def handle_user(e: any) -> None { username = e.target.value; }
|
|
289
|
+
def handle_pass(e: any) -> None { password = e.target.value; }
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div className="max-w-sm mx-auto">
|
|
293
|
+
<h1 className="text-2xl font-bold mb-6">Login</h1>
|
|
294
|
+
{error and <div className="p-3 mb-4 text-red-500">{error}</div>}
|
|
295
|
+
<form onSubmit={handleLogin} className="space-y-4">
|
|
296
|
+
<input value={username} onChange={handle_user} placeholder="Username"
|
|
297
|
+
className="w-full px-4 py-2 border rounded-lg" />
|
|
298
|
+
<input type="password" value={password} onChange={handle_pass}
|
|
299
|
+
placeholder="Password" className="w-full px-4 py-2 border rounded-lg" />
|
|
300
|
+
<button type="submit" disabled={loading}
|
|
301
|
+
className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">
|
|
302
|
+
{loading and "Logging in..." or "Login"}
|
|
303
|
+
</button>
|
|
304
|
+
</form>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## pages/(auth)/layout.jac — Auth Guard
|
|
312
|
+
|
|
313
|
+
```jac
|
|
314
|
+
cl import from "@jac/runtime" { AuthGuard, Outlet }
|
|
315
|
+
|
|
316
|
+
cl {
|
|
317
|
+
def:pub layout() -> JsxElement {
|
|
318
|
+
return <AuthGuard redirect="/login"><Outlet /></AuthGuard>;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|