jac-client 0.2.6__py3-none-any.whl → 0.2.8__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/examples/all-in-one/app.jac +573 -0
- jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
- jac_client/examples/all-in-one/components/Header.jac +13 -0
- jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
- jac_client/examples/all-in-one/components/Summary.jac +53 -0
- jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
- jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
- jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
- jac_client/examples/all-in-one/components/navigation.jac +132 -0
- jac_client/examples/all-in-one/constants/categories.jac +37 -0
- jac_client/examples/all-in-one/constants/clients.jac +13 -0
- jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
- jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
- jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
- jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
- jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
- jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
- jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
- jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
- jac_client/examples/all-in-one/pages/notFound.jac +24 -0
- jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
- jac_client/examples/all-in-one/utils/formatters.jac +52 -0
- jac_client/examples/asset-serving/css-with-image/src/app.jac +3 -3
- jac_client/examples/asset-serving/image-asset/src/app.jac +3 -3
- jac_client/examples/asset-serving/import-alias/src/app.jac +3 -3
- jac_client/examples/basic/src/app.jac +3 -3
- jac_client/examples/basic-auth/src/app.jac +31 -37
- jac_client/examples/basic-auth-with-router/src/app.jac +16 -16
- jac_client/examples/basic-full-stack/src/app.jac +24 -30
- jac_client/examples/css-styling/js-styling/src/app.jac +5 -5
- jac_client/examples/css-styling/material-ui/src/app.jac +5 -5
- jac_client/examples/css-styling/pure-css/src/app.jac +5 -5
- jac_client/examples/css-styling/sass-example/src/app.jac +5 -5
- jac_client/examples/css-styling/styled-components/src/app.jac +5 -5
- jac_client/examples/css-styling/tailwind-example/src/app.jac +5 -5
- jac_client/examples/full-stack-with-auth/src/app.jac +16 -16
- jac_client/examples/ts-support/src/app.jac +4 -4
- jac_client/examples/with-router/src/app.jac +4 -4
- jac_client/plugin/cli.jac +160 -203
- jac_client/plugin/client.jac +8 -15
- jac_client/plugin/client_runtime.cl.jac +18 -14
- jac_client/plugin/impl/client.impl.jac +85 -26
- jac_client/plugin/impl/client_runtime.impl.jac +27 -9
- jac_client/plugin/plugin_config.jac +11 -11
- jac_client/plugin/src/compiler.jac +2 -1
- jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
- jac_client/plugin/src/impl/compiler.impl.jac +55 -18
- jac_client/plugin/src/impl/vite_bundler.impl.jac +215 -102
- jac_client/plugin/src/package_installer.jac +1 -1
- jac_client/plugin/src/vite_bundler.jac +9 -1
- jac_client/tests/conftest.py +10 -8
- jac_client/tests/fixtures/spawn_test/app.jac +15 -18
- jac_client/tests/fixtures/with-ts/app.jac +4 -4
- jac_client/tests/test_cli.py +105 -49
- jac_client/tests/test_it.py +297 -82
- {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/METADATA +16 -7
- jac_client-0.2.8.dist-info/RECORD +97 -0
- jac_client/examples/all-in-one/src/app.jac +0 -841
- jac_client-0.2.6.dist-info/RECORD +0 -74
- /jac_client/examples/all-in-one/{src/button.jac → button.jac} +0 -0
- /jac_client/examples/all-in-one/{src/components → components}/button.jac +0 -0
- {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/WHEEL +0 -0
- {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Summary component - displays budget totals with business/personal breakdown
|
|
2
|
+
# Demonstrates: context usage, conditional rendering, ternary operator, 6-card layout
|
|
3
|
+
|
|
4
|
+
cl import from ..context.BudgetContext { useBudgetContext }
|
|
5
|
+
cl import from ..utils.formatters { formatCurrency }
|
|
6
|
+
|
|
7
|
+
cl {
|
|
8
|
+
def:pub Summary() -> any {
|
|
9
|
+
budget = useBudgetContext();
|
|
10
|
+
|
|
11
|
+
# Business/Personal breakdown
|
|
12
|
+
businessIncome = budget["businessIncome"];
|
|
13
|
+
businessExpenses = budget["businessExpenses"];
|
|
14
|
+
personalIncome = budget["personalIncome"];
|
|
15
|
+
personalExpenses = budget["personalExpenses"];
|
|
16
|
+
taxReserve = budget["taxReserve"];
|
|
17
|
+
netProfit = budget["netProfit"];
|
|
18
|
+
|
|
19
|
+
return <div className="summary">
|
|
20
|
+
<div className="summary-card business-income">
|
|
21
|
+
<span className="summary-label">Business Income</span>
|
|
22
|
+
<span className="summary-value">{formatCurrency(businessIncome)}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="summary-card business-expenses">
|
|
26
|
+
<span className="summary-label">Business Expenses</span>
|
|
27
|
+
<span className="summary-value">{formatCurrency(businessExpenses)}</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div className="summary-card personal-income">
|
|
31
|
+
<span className="summary-label">Personal Income</span>
|
|
32
|
+
<span className="summary-value">{formatCurrency(personalIncome)}</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="summary-card personal-expenses">
|
|
36
|
+
<span className="summary-label">Personal Expenses</span>
|
|
37
|
+
<span className="summary-value">{formatCurrency(personalExpenses)}</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="summary-card tax-reserve">
|
|
41
|
+
<span className="summary-label">Tax Reserve (20%)</span>
|
|
42
|
+
<span className="summary-value">{formatCurrency(taxReserve)}</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className={("summary-card net-profit positive") if netProfit >= 0 else ("summary-card net-profit negative")}>
|
|
46
|
+
<span className="summary-label">Net Profit</span>
|
|
47
|
+
<span className="summary-value">
|
|
48
|
+
{("+") if netProfit > 0 else ("")}{formatCurrency(netProfit)}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Transaction form component
|
|
2
|
+
# Demonstrates: form handling, useState, events, select options
|
|
3
|
+
|
|
4
|
+
cl import from ..context.BudgetContext { useBudgetContext }
|
|
5
|
+
cl import from ..constants.categories { CATEGORIES, CATEGORY_LABELS }
|
|
6
|
+
cl import from ..constants.clients { CLIENTS }
|
|
7
|
+
|
|
8
|
+
cl {
|
|
9
|
+
def:pub TransactionForm() -> any {
|
|
10
|
+
[description, setDescription] = useState("");
|
|
11
|
+
[amount, setAmount] = useState("");
|
|
12
|
+
[category, setCategory] = useState("OTHER");
|
|
13
|
+
[txType, setTxType] = useState("expense");
|
|
14
|
+
[isBusiness, setIsBusiness] = useState(false);
|
|
15
|
+
[clientName, setClientName] = useState("");
|
|
16
|
+
budget = useBudgetContext();
|
|
17
|
+
|
|
18
|
+
def handleSubmit(e: any) -> None {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
|
|
21
|
+
# Validate inputs
|
|
22
|
+
trimmedDesc = description.trim();
|
|
23
|
+
if trimmedDesc == "" or amount == "" {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
parsedAmount = parseFloat(amount);
|
|
28
|
+
if isNaN(parsedAmount) or parsedAmount <= 0 {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Add the transaction
|
|
33
|
+
budget["addTransaction"](trimmedDesc, parsedAmount, category, txType, isBusiness, clientName);
|
|
34
|
+
|
|
35
|
+
# Reset form
|
|
36
|
+
setDescription("");
|
|
37
|
+
setAmount("");
|
|
38
|
+
setCategory("OTHER");
|
|
39
|
+
setIsBusiness(false);
|
|
40
|
+
setClientName("");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Filter categories based on type (income only has INCOME category)
|
|
44
|
+
availableCategories = CATEGORIES.filter(lambda cat: str -> bool {
|
|
45
|
+
if txType == "income" {
|
|
46
|
+
return cat == "INCOME";
|
|
47
|
+
}
|
|
48
|
+
return cat != "INCOME";
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
# Show client dropdown only for business income
|
|
52
|
+
showClientDropdown = (txType == "income" and isBusiness);
|
|
53
|
+
|
|
54
|
+
return <form className="transaction-form" onSubmit={handleSubmit}>
|
|
55
|
+
<div className="form-row">
|
|
56
|
+
<div className="form-group type-toggle">
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
className={("toggle-btn active") if txType == "expense" else ("toggle-btn")}
|
|
60
|
+
onClick={lambda: setTxType("expense")}
|
|
61
|
+
>
|
|
62
|
+
Expense
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className={("toggle-btn active income") if txType == "income" else ("toggle-btn")}
|
|
67
|
+
onClick={lambda: setTxType("income")}
|
|
68
|
+
>
|
|
69
|
+
Income
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="form-group business-toggle">
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className={("toggle-btn active") if isBusiness else ("toggle-btn")}
|
|
77
|
+
onClick={lambda: setIsBusiness(true)}
|
|
78
|
+
>
|
|
79
|
+
Business
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className={("toggle-btn active") if not isBusiness else ("toggle-btn")}
|
|
84
|
+
onClick={lambda: setIsBusiness(false)}
|
|
85
|
+
>
|
|
86
|
+
Personal
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="form-row">
|
|
92
|
+
<div className="form-group">
|
|
93
|
+
<label htmlFor="description">Description</label>
|
|
94
|
+
<input
|
|
95
|
+
id="description"
|
|
96
|
+
type="text"
|
|
97
|
+
value={description}
|
|
98
|
+
onChange={lambda e: any -> None { setDescription(e.target.value); }}
|
|
99
|
+
placeholder="Enter description..."
|
|
100
|
+
className="form-input"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="form-group">
|
|
105
|
+
<label htmlFor="amount">Amount</label>
|
|
106
|
+
<input
|
|
107
|
+
id="amount"
|
|
108
|
+
type="number"
|
|
109
|
+
value={amount}
|
|
110
|
+
onChange={lambda e: any -> None { setAmount(e.target.value); }}
|
|
111
|
+
placeholder="0.00"
|
|
112
|
+
min="0"
|
|
113
|
+
step="0.01"
|
|
114
|
+
className="form-input"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="form-group">
|
|
119
|
+
<label htmlFor="category">Category</label>
|
|
120
|
+
<select
|
|
121
|
+
id="category"
|
|
122
|
+
value={category}
|
|
123
|
+
onChange={lambda e: any -> None { setCategory(e.target.value); }}
|
|
124
|
+
className="form-select"
|
|
125
|
+
>
|
|
126
|
+
{availableCategories.map(lambda cat: str -> any {
|
|
127
|
+
return <option key={cat} value={cat}>
|
|
128
|
+
{CATEGORY_LABELS[cat]}
|
|
129
|
+
</option>;
|
|
130
|
+
})}
|
|
131
|
+
</select>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{showClientDropdown and <div className="form-group">
|
|
135
|
+
<label htmlFor="client">Client</label>
|
|
136
|
+
<select
|
|
137
|
+
id="client"
|
|
138
|
+
value={clientName}
|
|
139
|
+
onChange={lambda e: any -> None { setClientName(e.target.value); }}
|
|
140
|
+
className="form-select"
|
|
141
|
+
>
|
|
142
|
+
<option value="">Select Client (Optional)</option>
|
|
143
|
+
{CLIENTS.map(lambda client: str -> any {
|
|
144
|
+
return <option key={client} value={client}>{client}</option>;
|
|
145
|
+
})}
|
|
146
|
+
</select>
|
|
147
|
+
</div>}
|
|
148
|
+
|
|
149
|
+
<div className="form-group">
|
|
150
|
+
<label>Action</label>
|
|
151
|
+
<button type="submit" className="submit-btn">
|
|
152
|
+
Add {(txType[0].toUpperCase() + txType.slice(1))}
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</form>;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Single transaction item component
|
|
2
|
+
# Demonstrates: NEW PROPS PATTERN (direct parameters, NOT props dict)
|
|
3
|
+
|
|
4
|
+
cl import from ..utils.formatters { formatCurrency, formatDate }
|
|
5
|
+
cl import from ..constants.categories { CATEGORY_COLORS, CATEGORY_LABELS }
|
|
6
|
+
|
|
7
|
+
cl {
|
|
8
|
+
# NEW WAY: Direct parameters instead of props: dict
|
|
9
|
+
def:pub TransactionItem(
|
|
10
|
+
id: str,
|
|
11
|
+
description: str,
|
|
12
|
+
amount: float,
|
|
13
|
+
category: str,
|
|
14
|
+
txType: str,
|
|
15
|
+
date: str,
|
|
16
|
+
isBusiness: bool,
|
|
17
|
+
clientName: any,
|
|
18
|
+
onDelete: any
|
|
19
|
+
) -> any {
|
|
20
|
+
isIncome = txType == "income";
|
|
21
|
+
color = CATEGORY_COLORS[category];
|
|
22
|
+
label = CATEGORY_LABELS[category];
|
|
23
|
+
|
|
24
|
+
return <div className="transaction-item">
|
|
25
|
+
<div className="tx-left">
|
|
26
|
+
<span
|
|
27
|
+
className="tx-category"
|
|
28
|
+
style={{"backgroundColor": color}}
|
|
29
|
+
>
|
|
30
|
+
{label}
|
|
31
|
+
</span>
|
|
32
|
+
<div className="tx-details">
|
|
33
|
+
<span className="tx-description">
|
|
34
|
+
{description}
|
|
35
|
+
{(clientName != None and isIncome) and <span className="tx-client"> • {clientName}</span>}
|
|
36
|
+
</span>
|
|
37
|
+
<span className="tx-date">{formatDate(date)}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="tx-right">
|
|
42
|
+
<span className={("tx-amount income") if isIncome else ("tx-amount expense")}>
|
|
43
|
+
{("+") if isIncome else ("-")}{formatCurrency(amount)}
|
|
44
|
+
</span>
|
|
45
|
+
<button
|
|
46
|
+
className="delete-btn"
|
|
47
|
+
onClick={lambda:onDelete(id)}
|
|
48
|
+
title="Delete transaction"
|
|
49
|
+
>
|
|
50
|
+
X
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Transaction list component
|
|
2
|
+
# Demonstrates: rendering lists with map, passing props to child components
|
|
3
|
+
|
|
4
|
+
cl import from .TransactionItem { TransactionItem }
|
|
5
|
+
|
|
6
|
+
cl {
|
|
7
|
+
# Props: transactions list and delete handler
|
|
8
|
+
def:pub TransactionList(transactions: list, onDelete: any) -> any {
|
|
9
|
+
if transactions.length == 0 {
|
|
10
|
+
return <div className="empty-state">
|
|
11
|
+
<p>No transactions yet.</p>
|
|
12
|
+
<p>Add your first income or expense above!</p>
|
|
13
|
+
</div>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return <div className="transaction-list">
|
|
17
|
+
<h3 className="list-title">
|
|
18
|
+
Transactions ({transactions.length})
|
|
19
|
+
</h3>
|
|
20
|
+
|
|
21
|
+
{transactions.map(lambda tx: dict -> any {
|
|
22
|
+
return <TransactionItem
|
|
23
|
+
key={tx["id"]}
|
|
24
|
+
id={tx["id"]}
|
|
25
|
+
description={tx["description"]}
|
|
26
|
+
amount={tx["amount"]}
|
|
27
|
+
category={tx["category"]}
|
|
28
|
+
txType={tx["type"]}
|
|
29
|
+
date={tx["date"]}
|
|
30
|
+
isBusiness={tx["isBusinessTransaction"] || false}
|
|
31
|
+
clientName={tx["clientName"] || None}
|
|
32
|
+
onDelete={onDelete}
|
|
33
|
+
/>;
|
|
34
|
+
})}
|
|
35
|
+
</div>;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
cl import from "@jac-client/utils" {
|
|
2
|
+
|
|
3
|
+
Link,
|
|
4
|
+
useNavigate,
|
|
5
|
+
useLocation,
|
|
6
|
+
jacLogout,
|
|
7
|
+
jacIsLoggedIn
|
|
8
|
+
}
|
|
9
|
+
cl {
|
|
10
|
+
def:pub Navigation -> any {
|
|
11
|
+
location = useLocation();
|
|
12
|
+
isLoggedIn = jacIsLoggedIn();
|
|
13
|
+
navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
def linkStyle(path: str) -> dict {
|
|
16
|
+
isActive = location.pathname == path;
|
|
17
|
+
return {
|
|
18
|
+
"padding": "0.5rem 1rem",
|
|
19
|
+
"textDecoration": "none",
|
|
20
|
+
"color": "#0066cc" if isActive else "#333",
|
|
21
|
+
"fontWeight": "bold" if isActive else "normal",
|
|
22
|
+
"backgroundColor": "#e3f2fd" if isActive else "transparent",
|
|
23
|
+
"borderRadius": "4px",
|
|
24
|
+
"display": "inline-block"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def handleLogout(e: any) -> None {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
jacLogout();
|
|
31
|
+
navigate("/login");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
authButtons = None;
|
|
35
|
+
if isLoggedIn {
|
|
36
|
+
authButtons = <button
|
|
37
|
+
onClick={handleLogout}
|
|
38
|
+
style={{
|
|
39
|
+
"padding": "0.5rem 1rem",
|
|
40
|
+
"background": "#ef4444",
|
|
41
|
+
"color": "#ffffff",
|
|
42
|
+
"border": "none",
|
|
43
|
+
"borderRadius": "4px",
|
|
44
|
+
"cursor": "pointer",
|
|
45
|
+
"fontWeight": "600"
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
Logout
|
|
49
|
+
</button>;
|
|
50
|
+
} else {
|
|
51
|
+
authButtons = <>
|
|
52
|
+
<Link
|
|
53
|
+
to="/login"
|
|
54
|
+
style={linkStyle("/login")}
|
|
55
|
+
>
|
|
56
|
+
Login
|
|
57
|
+
</Link>
|
|
58
|
+
<Link
|
|
59
|
+
to="/signup"
|
|
60
|
+
style={linkStyle("/signup")}
|
|
61
|
+
>
|
|
62
|
+
Sign Up
|
|
63
|
+
</Link>
|
|
64
|
+
</>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return <nav
|
|
68
|
+
style={{
|
|
69
|
+
"padding": "1rem",
|
|
70
|
+
"backgroundColor": "#f5f5f5",
|
|
71
|
+
"marginBottom": "2rem",
|
|
72
|
+
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
"maxWidth": "1200px",
|
|
78
|
+
"margin": "0 auto",
|
|
79
|
+
"display": "flex",
|
|
80
|
+
"gap": "1rem",
|
|
81
|
+
"alignItems": "center",
|
|
82
|
+
"justifyContent": "space-between"
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
style={{"display": "flex", "gap": "1rem", "alignItems": "center"}}
|
|
87
|
+
>
|
|
88
|
+
{isLoggedIn
|
|
89
|
+
and (
|
|
90
|
+
<>
|
|
91
|
+
<Link
|
|
92
|
+
to="/"
|
|
93
|
+
style={linkStyle("/")}
|
|
94
|
+
>
|
|
95
|
+
Home
|
|
96
|
+
</Link>
|
|
97
|
+
<Link
|
|
98
|
+
to="/nested"
|
|
99
|
+
style={linkStyle("/nested")}
|
|
100
|
+
>
|
|
101
|
+
Nested Imports
|
|
102
|
+
</Link>
|
|
103
|
+
<Link
|
|
104
|
+
to="/features-test"
|
|
105
|
+
style={linkStyle("/features-test")}
|
|
106
|
+
>
|
|
107
|
+
Features Test
|
|
108
|
+
</Link>
|
|
109
|
+
<Link
|
|
110
|
+
to="/landing"
|
|
111
|
+
style={linkStyle("/landing")}
|
|
112
|
+
>
|
|
113
|
+
Landing Page
|
|
114
|
+
</Link>
|
|
115
|
+
<Link
|
|
116
|
+
to="/budget-planner"
|
|
117
|
+
style={linkStyle("/budget-planner")}
|
|
118
|
+
>
|
|
119
|
+
Budget Planner
|
|
120
|
+
</Link>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
<div
|
|
125
|
+
style={{"display": "flex", "gap": "1rem", "alignItems": "center"}}
|
|
126
|
+
>
|
|
127
|
+
{authButtons}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</nav>;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Category constants and enums for Budget Planner
|
|
2
|
+
# Demonstrates: enum:pub, glob:pub exports
|
|
3
|
+
|
|
4
|
+
cl {
|
|
5
|
+
# Category enum - demonstrates enum:pub export
|
|
6
|
+
enum:pub Category {
|
|
7
|
+
INCOME,
|
|
8
|
+
FOOD,
|
|
9
|
+
TRANSPORT,
|
|
10
|
+
UTILITIES,
|
|
11
|
+
ENTERTAINMENT,
|
|
12
|
+
OTHER
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Category colors - demonstrates glob:pub export
|
|
16
|
+
glob:pub CATEGORY_COLORS: dict = {
|
|
17
|
+
"INCOME": "#10b981",
|
|
18
|
+
"FOOD": "#f59e0b",
|
|
19
|
+
"TRANSPORT": "#3b82f6",
|
|
20
|
+
"UTILITIES": "#8b5cf6",
|
|
21
|
+
"ENTERTAINMENT": "#ec4899",
|
|
22
|
+
"OTHER": "#6b7280"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
# Category labels for display
|
|
26
|
+
glob:pub CATEGORY_LABELS: dict = {
|
|
27
|
+
"INCOME": "Income",
|
|
28
|
+
"FOOD": "Food & Dining",
|
|
29
|
+
"TRANSPORT": "Transport",
|
|
30
|
+
"UTILITIES": "Utilities",
|
|
31
|
+
"ENTERTAINMENT": "Entertainment",
|
|
32
|
+
"OTHER": "Other"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
# All category keys as a list
|
|
36
|
+
glob:pub CATEGORIES: list = ["INCOME", "FOOD", "TRANSPORT", "UTILITIES", "ENTERTAINMENT", "OTHER"];
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Budget Context for global state management
|
|
2
|
+
# Demonstrates: createContext, useContext, Context.Provider
|
|
3
|
+
|
|
4
|
+
cl import from react { createContext, useContext }
|
|
5
|
+
cl import from ..hooks.useBudget { useBudget }
|
|
6
|
+
|
|
7
|
+
cl {
|
|
8
|
+
# Create the context - demonstrates glob for context
|
|
9
|
+
glob:pub BudgetContext = createContext(None);
|
|
10
|
+
|
|
11
|
+
# Provider component - wraps app and provides budget state
|
|
12
|
+
def:pub BudgetProvider(children: any) -> any {
|
|
13
|
+
budget = useBudget();
|
|
14
|
+
|
|
15
|
+
return <BudgetContext.Provider value={budget}>
|
|
16
|
+
{children}
|
|
17
|
+
</BudgetContext.Provider>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Custom hook to access budget context
|
|
21
|
+
def:pub useBudgetContext() -> dict {
|
|
22
|
+
context = useContext(BudgetContext);
|
|
23
|
+
if context == None {
|
|
24
|
+
console.error("useBudgetContext must be used within BudgetProvider");
|
|
25
|
+
}
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Custom hook for budget operations
|
|
2
|
+
# Demonstrates: custom hooks, useState, array methods
|
|
3
|
+
|
|
4
|
+
cl import from react { useCallback, useMemo }
|
|
5
|
+
|
|
6
|
+
cl {
|
|
7
|
+
# Main budget management hook
|
|
8
|
+
def:pub useBudget() -> dict {
|
|
9
|
+
# Simple useState - no complex localStorage hook
|
|
10
|
+
[transactions, setTransactions] = useState([]);
|
|
11
|
+
|
|
12
|
+
# Add a new transaction
|
|
13
|
+
def addTransaction(description: str, amount: float, category: str, txType: str, isBusiness: bool, clientName: str) -> None {
|
|
14
|
+
newTx = {
|
|
15
|
+
"id": Date.now().toString(),
|
|
16
|
+
"description": description,
|
|
17
|
+
"amount": amount,
|
|
18
|
+
"category": category,
|
|
19
|
+
"type": txType,
|
|
20
|
+
"date": Reflect.construct(Date, []).toISOString(),
|
|
21
|
+
"isBusinessTransaction": isBusiness,
|
|
22
|
+
"clientName": clientName if clientName != "" else None
|
|
23
|
+
};
|
|
24
|
+
setTransactions(transactions.concat([newTx]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Delete a transaction by ID
|
|
28
|
+
def deleteTransaction(id: str) -> None {
|
|
29
|
+
filtered = transactions.filter(lambda tx: dict -> bool {
|
|
30
|
+
return tx["id"] != id;
|
|
31
|
+
});
|
|
32
|
+
setTransactions(filtered);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Calculate totals
|
|
36
|
+
totalIncome = 0.0;
|
|
37
|
+
totalExpenses = 0.0;
|
|
38
|
+
for tx in transactions {
|
|
39
|
+
if tx["type"] == "income" {
|
|
40
|
+
totalIncome = totalIncome + tx["amount"];
|
|
41
|
+
} else {
|
|
42
|
+
totalExpenses = totalExpenses + tx["amount"];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
balance = totalIncome - totalExpenses;
|
|
46
|
+
|
|
47
|
+
# Calculate business/personal breakdown
|
|
48
|
+
businessIncome = 0.0;
|
|
49
|
+
businessExpenses = 0.0;
|
|
50
|
+
personalIncome = 0.0;
|
|
51
|
+
personalExpenses = 0.0;
|
|
52
|
+
|
|
53
|
+
for tx in transactions {
|
|
54
|
+
isBusiness = tx["isBusinessTransaction"] || false;
|
|
55
|
+
amount = tx["amount"];
|
|
56
|
+
|
|
57
|
+
if tx["type"] == "income" {
|
|
58
|
+
if isBusiness {
|
|
59
|
+
businessIncome = businessIncome + amount;
|
|
60
|
+
} else {
|
|
61
|
+
personalIncome = personalIncome + amount;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
if isBusiness {
|
|
65
|
+
businessExpenses = businessExpenses + amount;
|
|
66
|
+
} else {
|
|
67
|
+
personalExpenses = personalExpenses + amount;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Tax calculations
|
|
73
|
+
TAX_RATE = 0.20;
|
|
74
|
+
taxReserve = businessIncome * TAX_RATE;
|
|
75
|
+
netProfit = businessIncome - businessExpenses - taxReserve;
|
|
76
|
+
|
|
77
|
+
# Get expense breakdown by category (for chart)
|
|
78
|
+
def getExpensesByCategory() -> list {
|
|
79
|
+
categoryTotals = {};
|
|
80
|
+
for tx in transactions {
|
|
81
|
+
if tx["type"] == "expense" {
|
|
82
|
+
cat = tx["category"];
|
|
83
|
+
if categoryTotals[cat] {
|
|
84
|
+
categoryTotals[cat] = categoryTotals[cat] + tx["amount"];
|
|
85
|
+
} else {
|
|
86
|
+
categoryTotals[cat] = tx["amount"];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
result = [];
|
|
91
|
+
for key in Object.keys(categoryTotals) {
|
|
92
|
+
result = result.concat([{
|
|
93
|
+
"name": key,
|
|
94
|
+
"value": categoryTotals[key]
|
|
95
|
+
}]);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"transactions": transactions,
|
|
102
|
+
"addTransaction": addTransaction,
|
|
103
|
+
"deleteTransaction": deleteTransaction,
|
|
104
|
+
"totalIncome": totalIncome,
|
|
105
|
+
"totalExpenses": totalExpenses,
|
|
106
|
+
"balance": balance,
|
|
107
|
+
"expensesByCategory": getExpensesByCategory(),
|
|
108
|
+
"businessIncome": businessIncome,
|
|
109
|
+
"businessExpenses": businessExpenses,
|
|
110
|
+
"personalIncome": personalIncome,
|
|
111
|
+
"personalExpenses": personalExpenses,
|
|
112
|
+
"taxReserve": taxReserve,
|
|
113
|
+
"netProfit": netProfit
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Custom hook for localStorage persistence
|
|
2
|
+
# Demonstrates: custom hooks, try/except error handling, useEffect
|
|
3
|
+
|
|
4
|
+
cl import from react { useEffect }
|
|
5
|
+
|
|
6
|
+
cl {
|
|
7
|
+
# Custom hook for syncing state with localStorage
|
|
8
|
+
def useLocalStorage(key: str, initialValue: list) -> list {
|
|
9
|
+
# Initialize state with value from localStorage or default
|
|
10
|
+
[storedValue, setStoredValue] = useState(lambda -> any {
|
|
11
|
+
try {
|
|
12
|
+
item = window.localStorage.getItem(key);
|
|
13
|
+
if item {
|
|
14
|
+
return JSON.parse(item);
|
|
15
|
+
}else{
|
|
16
|
+
return initialValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
} except Exception as e {
|
|
20
|
+
console.log("Error reading from localStorage");
|
|
21
|
+
return initialValue;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
# Update localStorage when state changes
|
|
26
|
+
useEffect(lambda -> None {
|
|
27
|
+
try {
|
|
28
|
+
window.localStorage.setItem(key, JSON.stringify(storedValue));
|
|
29
|
+
} except Exception as e {
|
|
30
|
+
console.log("Error saving to localStorage");
|
|
31
|
+
}
|
|
32
|
+
}, [key, storedValue]);
|
|
33
|
+
|
|
34
|
+
return [storedValue, setStoredValue];
|
|
35
|
+
}
|
|
36
|
+
}
|