@kaiserofthenight/human-js 1.0.0
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.
- package/README.md +545 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +201 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +66 -0
- package/src/core/component.js +182 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +151 -0
- package/src/core/router.js +182 -0
- package/src/core/state.js +114 -0
- package/src/index.js +63 -0
- package/src/plugins/http.js +167 -0
- package/src/plugins/storage.js +181 -0
- package/src/plugins/validator.js +193 -0
- package/src/utils/dom.js +0 -0
- package/src/utils/helpers.js +209 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API INTEGRATION EXAMPLE
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Fetch data from API
|
|
6
|
+
* - Loading states
|
|
7
|
+
* - Error handling
|
|
8
|
+
* - Search and filter
|
|
9
|
+
* - Pagination
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { app, html, each, when } from '../../src/index.js';
|
|
13
|
+
import { createHttp } from '../../src/plugins/http.js';
|
|
14
|
+
import { debounce } from '../../src/core/events.js';
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// HTTP CLIENT
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
const api = createHttp({
|
|
21
|
+
baseURL: 'https://jsonplaceholder.typicode.com',
|
|
22
|
+
onRequest: (config) => {
|
|
23
|
+
console.log('🌐 API Request:', config);
|
|
24
|
+
},
|
|
25
|
+
onError: (error) => {
|
|
26
|
+
console.error('❌ API Error:', error);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// COMPONENTS
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
function UserCard(user) {
|
|
35
|
+
return html`
|
|
36
|
+
<div style="
|
|
37
|
+
border: 1px solid #ddd;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
padding: 20px;
|
|
40
|
+
margin: 10px;
|
|
41
|
+
background: white;
|
|
42
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
43
|
+
transition: transform 0.2s;
|
|
44
|
+
"
|
|
45
|
+
onmouseover="this.style.transform='translateY(-4px)'"
|
|
46
|
+
onmouseout="this.style.transform='translateY(0)'"
|
|
47
|
+
>
|
|
48
|
+
<h3 style="margin: 0 0 10px 0; color: #2196F3;">
|
|
49
|
+
${user.name}
|
|
50
|
+
</h3>
|
|
51
|
+
<p style="margin: 5px 0; color: #666;">
|
|
52
|
+
📧 ${user.email}
|
|
53
|
+
</p>
|
|
54
|
+
<p style="margin: 5px 0; color: #666;">
|
|
55
|
+
📱 ${user.phone}
|
|
56
|
+
</p>
|
|
57
|
+
<p style="margin: 5px 0; color: #666;">
|
|
58
|
+
🏢 ${user.company.name}
|
|
59
|
+
</p>
|
|
60
|
+
<button
|
|
61
|
+
class="view-posts-btn"
|
|
62
|
+
data-user-id="${user.id}"
|
|
63
|
+
style="
|
|
64
|
+
margin-top: 10px;
|
|
65
|
+
padding: 8px 16px;
|
|
66
|
+
background: #4CAF50;
|
|
67
|
+
color: white;
|
|
68
|
+
border: none;
|
|
69
|
+
border-radius: 4px;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
"
|
|
72
|
+
>
|
|
73
|
+
View Posts
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function PostCard(post) {
|
|
80
|
+
return html`
|
|
81
|
+
<div style="
|
|
82
|
+
border-left: 4px solid #2196F3;
|
|
83
|
+
padding: 15px;
|
|
84
|
+
margin: 10px 0;
|
|
85
|
+
background: #f9f9f9;
|
|
86
|
+
border-radius: 4px;
|
|
87
|
+
">
|
|
88
|
+
<h4 style="margin: 0 0 10px 0; color: #333;">
|
|
89
|
+
${post.title}
|
|
90
|
+
</h4>
|
|
91
|
+
<p style="margin: 0; color: #666; line-height: 1.6;">
|
|
92
|
+
${post.body}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function LoadingSpinner() {
|
|
99
|
+
return html`
|
|
100
|
+
<div style="
|
|
101
|
+
text-align: center;
|
|
102
|
+
padding: 40px;
|
|
103
|
+
">
|
|
104
|
+
<div style="
|
|
105
|
+
display: inline-block;
|
|
106
|
+
width: 40px;
|
|
107
|
+
height: 40px;
|
|
108
|
+
border: 4px solid #f3f3f3;
|
|
109
|
+
border-top: 4px solid #2196F3;
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
animation: spin 1s linear infinite;
|
|
112
|
+
"></div>
|
|
113
|
+
<style>
|
|
114
|
+
@keyframes spin {
|
|
115
|
+
0% { transform: rotate(0deg); }
|
|
116
|
+
100% { transform: rotate(360deg); }
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
119
|
+
<p style="margin-top: 20px; color: #666;">Loading...</p>
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ErrorMessage(message) {
|
|
125
|
+
return html`
|
|
126
|
+
<div style="
|
|
127
|
+
padding: 20px;
|
|
128
|
+
background: #ffebee;
|
|
129
|
+
border-left: 4px solid #f44336;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
margin: 20px 0;
|
|
132
|
+
">
|
|
133
|
+
<h3 style="margin: 0 0 10px 0; color: #c62828;">
|
|
134
|
+
⚠️ Error
|
|
135
|
+
</h3>
|
|
136
|
+
<p style="margin: 0; color: #666;">
|
|
137
|
+
${message}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================
|
|
144
|
+
// MAIN APP
|
|
145
|
+
// ============================================
|
|
146
|
+
|
|
147
|
+
const apiApp = app.create({
|
|
148
|
+
state: {
|
|
149
|
+
users: [],
|
|
150
|
+
posts: [],
|
|
151
|
+
loading: false,
|
|
152
|
+
error: null,
|
|
153
|
+
view: 'users', // users, posts
|
|
154
|
+
selectedUserId: null,
|
|
155
|
+
searchQuery: ''
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
render: (state) => {
|
|
159
|
+
// Filter users by search query
|
|
160
|
+
const filteredUsers = state.users.filter(user =>
|
|
161
|
+
user.name.toLowerCase().includes(state.searchQuery.toLowerCase()) ||
|
|
162
|
+
user.email.toLowerCase().includes(state.searchQuery.toLowerCase())
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const element = html`
|
|
166
|
+
<div style="
|
|
167
|
+
max-width: 1200px;
|
|
168
|
+
margin: 50px auto;
|
|
169
|
+
padding: 20px;
|
|
170
|
+
font-family: system-ui, sans-serif;
|
|
171
|
+
">
|
|
172
|
+
<h1 style="text-align: center; color: #2196F3;">
|
|
173
|
+
🌐 API Integration Example
|
|
174
|
+
</h1>
|
|
175
|
+
|
|
176
|
+
<!-- Navigation -->
|
|
177
|
+
<div style="
|
|
178
|
+
display: flex;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
gap: 10px;
|
|
181
|
+
margin: 30px 0;
|
|
182
|
+
">
|
|
183
|
+
<button
|
|
184
|
+
id="load-users-btn"
|
|
185
|
+
style="
|
|
186
|
+
padding: 12px 24px;
|
|
187
|
+
background: ${state.view === 'users' ? '#2196F3' : '#fff'};
|
|
188
|
+
color: ${state.view === 'users' ? '#fff' : '#2196F3'};
|
|
189
|
+
border: 2px solid #2196F3;
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
font-weight: bold;
|
|
193
|
+
"
|
|
194
|
+
>
|
|
195
|
+
Load Users
|
|
196
|
+
</button>
|
|
197
|
+
${when(
|
|
198
|
+
state.selectedUserId,
|
|
199
|
+
() => html`
|
|
200
|
+
<button
|
|
201
|
+
id="back-to-users-btn"
|
|
202
|
+
style="
|
|
203
|
+
padding: 12px 24px;
|
|
204
|
+
background: #FF9800;
|
|
205
|
+
color: white;
|
|
206
|
+
border: none;
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
"
|
|
210
|
+
>
|
|
211
|
+
← Back to Users
|
|
212
|
+
</button>
|
|
213
|
+
`
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Search Bar (only for users view) -->
|
|
218
|
+
${when(
|
|
219
|
+
state.view === 'users' && state.users.length > 0,
|
|
220
|
+
() => html`
|
|
221
|
+
<div style="margin: 20px 0;">
|
|
222
|
+
<input
|
|
223
|
+
type="text"
|
|
224
|
+
id="search-input"
|
|
225
|
+
placeholder="Search users by name or email..."
|
|
226
|
+
value="${state.searchQuery}"
|
|
227
|
+
style="
|
|
228
|
+
width: 100%;
|
|
229
|
+
padding: 12px;
|
|
230
|
+
border: 2px solid #ddd;
|
|
231
|
+
border-radius: 4px;
|
|
232
|
+
font-size: 16px;
|
|
233
|
+
"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
`
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<!-- Loading State -->
|
|
240
|
+
${when(
|
|
241
|
+
state.loading,
|
|
242
|
+
() => LoadingSpinner()
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
<!-- Error State -->
|
|
246
|
+
${when(
|
|
247
|
+
state.error,
|
|
248
|
+
() => ErrorMessage(state.error)
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
<!-- Users Grid -->
|
|
252
|
+
${when(
|
|
253
|
+
state.view === 'users' && !state.loading && !state.error,
|
|
254
|
+
() => html`
|
|
255
|
+
<div>
|
|
256
|
+
${when(
|
|
257
|
+
filteredUsers.length > 0,
|
|
258
|
+
() => html`
|
|
259
|
+
<div style="
|
|
260
|
+
display: grid;
|
|
261
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
262
|
+
gap: 20px;
|
|
263
|
+
">
|
|
264
|
+
${each(filteredUsers, (user) => UserCard(user))}
|
|
265
|
+
</div>
|
|
266
|
+
`,
|
|
267
|
+
() => html`
|
|
268
|
+
<p style="text-align: center; color: #999; padding: 40px;">
|
|
269
|
+
${state.searchQuery ? 'No users found matching your search.' : 'No users loaded. Click "Load Users" to fetch data.'}
|
|
270
|
+
</p>
|
|
271
|
+
`
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
`
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
<!-- Posts View -->
|
|
278
|
+
${when(
|
|
279
|
+
state.view === 'posts' && !state.loading && !state.error,
|
|
280
|
+
() => html`
|
|
281
|
+
<div>
|
|
282
|
+
<h2 style="color: #333; margin-bottom: 20px;">
|
|
283
|
+
User Posts (${state.posts.length})
|
|
284
|
+
</h2>
|
|
285
|
+
${when(
|
|
286
|
+
state.posts.length > 0,
|
|
287
|
+
() => html`
|
|
288
|
+
<div>
|
|
289
|
+
${each(state.posts, (post) => PostCard(post))}
|
|
290
|
+
</div>
|
|
291
|
+
`,
|
|
292
|
+
() => html`
|
|
293
|
+
<p style="text-align: center; color: #999; padding: 40px;">
|
|
294
|
+
No posts found for this user.
|
|
295
|
+
</p>
|
|
296
|
+
`
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
`
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
`;
|
|
303
|
+
|
|
304
|
+
// Debounced search handler
|
|
305
|
+
const handleSearch = debounce((e) => {
|
|
306
|
+
state.searchQuery = e.target.value;
|
|
307
|
+
}, 300);
|
|
308
|
+
|
|
309
|
+
const events = {
|
|
310
|
+
'#load-users-btn': {
|
|
311
|
+
click: async () => {
|
|
312
|
+
state.loading = true;
|
|
313
|
+
state.error = null;
|
|
314
|
+
state.view = 'users';
|
|
315
|
+
state.selectedUserId = null;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const { data } = await api.get('/users');
|
|
319
|
+
state.users = data;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
state.error = 'Failed to load users. Please try again.';
|
|
322
|
+
} finally {
|
|
323
|
+
state.loading = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
'.view-posts-btn': {
|
|
328
|
+
click: async (e) => {
|
|
329
|
+
const userId = e.target.dataset.userId;
|
|
330
|
+
state.selectedUserId = userId;
|
|
331
|
+
state.loading = true;
|
|
332
|
+
state.error = null;
|
|
333
|
+
state.view = 'posts';
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const { data } = await api.get(`/posts?userId=${userId}`);
|
|
337
|
+
state.posts = data;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
state.error = 'Failed to load posts. Please try again.';
|
|
340
|
+
} finally {
|
|
341
|
+
state.loading = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
'#back-to-users-btn': {
|
|
346
|
+
click: () => {
|
|
347
|
+
state.view = 'users';
|
|
348
|
+
state.selectedUserId = null;
|
|
349
|
+
state.posts = [];
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
'#search-input': {
|
|
353
|
+
input: handleSearch
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return { element, events };
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
onMount: (state) => {
|
|
361
|
+
console.log('✅ API Example App mounted!');
|
|
362
|
+
// Auto-load users on mount
|
|
363
|
+
document.getElementById('load-users-btn')?.click();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { app, html } from '../../src/index.js';
|
|
2
|
+
import { local } from '../../src/plugins/storage.js';
|
|
3
|
+
|
|
4
|
+
app.create({
|
|
5
|
+
state: {
|
|
6
|
+
count: 0,
|
|
7
|
+
step: 1,
|
|
8
|
+
history: []
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
onMount(state) {
|
|
12
|
+
// Load saved counter from localStorage
|
|
13
|
+
const saved = local.get('counterState');
|
|
14
|
+
if (saved) {
|
|
15
|
+
state.count = saved.count || 0;
|
|
16
|
+
state.step = saved.step || 1;
|
|
17
|
+
state.history = saved.history || [];
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
render(state) {
|
|
22
|
+
const element = html`
|
|
23
|
+
<div style="min-height: 100vh; background: #f5f5f5; display: flex; align-items: center; justify-content: center; padding: 20px;">
|
|
24
|
+
<div style="width: 100%; max-width: 500px;">
|
|
25
|
+
|
|
26
|
+
<!-- Main Counter Card -->
|
|
27
|
+
<div style="background: #ffffff; border-radius: 8px; padding: 40px; text-align: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 20px;">
|
|
28
|
+
<h1 style="color: #212121; font-size: 20px; margin: 0 0 30px 0; font-weight: 500;">Simple Counter</h1>
|
|
29
|
+
|
|
30
|
+
<!-- Counter Display -->
|
|
31
|
+
<div style="margin: 30px 0;">
|
|
32
|
+
<div style="color: #2196f3; font-size: 72px; font-weight: 600; line-height: 1;">
|
|
33
|
+
${state.count}
|
|
34
|
+
</div>
|
|
35
|
+
<p style="color: #9e9e9e; font-size: 14px; margin: 10px 0 0 0;">Current Count</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Main Buttons -->
|
|
39
|
+
<div style="display: flex; gap: 12px; margin: 30px 0;">
|
|
40
|
+
<button
|
|
41
|
+
id="decrement-btn"
|
|
42
|
+
style="flex: 1; padding: 16px; background: #f44336; border: none; border-radius: 4px; color: #ffffff; font-size: 16px; font-weight: 500; cursor: pointer;">
|
|
43
|
+
- ${state.step}
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
id="reset-btn"
|
|
47
|
+
style="flex: 1; padding: 16px; background: #9e9e9e; border: none; border-radius: 4px; color: #ffffff; font-size: 16px; font-weight: 500; cursor: pointer;">
|
|
48
|
+
Reset
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
id="increment-btn"
|
|
52
|
+
style="flex: 1; padding: 16px; background: #4caf50; border: none; border-radius: 4px; color: #ffffff; font-size: 16px; font-weight: 500; cursor: pointer;">
|
|
53
|
+
+ ${state.step}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Step Control Card -->
|
|
59
|
+
<div style="background: #ffffff; border-radius: 8px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 20px;">
|
|
60
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
61
|
+
<label style="color: #424242; font-size: 14px; font-weight: 500;">Step Size</label>
|
|
62
|
+
<span style="color: #2196f3; font-size: 18px; font-weight: 600;">${state.step}</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div style="display: flex; gap: 8px;">
|
|
66
|
+
<button
|
|
67
|
+
id="step-1"
|
|
68
|
+
class="step-btn"
|
|
69
|
+
style="flex: 1; padding: 10px; background: ${state.step === 1 ? '#2196f3' : '#ffffff'}; border: 1px solid #e0e0e0; border-radius: 4px; color: ${state.step === 1 ? '#ffffff' : '#757575'}; font-size: 14px; cursor: pointer;">
|
|
70
|
+
1
|
|
71
|
+
</button>
|
|
72
|
+
<button
|
|
73
|
+
id="step-5"
|
|
74
|
+
class="step-btn"
|
|
75
|
+
style="flex: 1; padding: 10px; background: ${state.step === 5 ? '#2196f3' : '#ffffff'}; border: 1px solid #e0e0e0; border-radius: 4px; color: ${state.step === 5 ? '#ffffff' : '#757575'}; font-size: 14px; cursor: pointer;">
|
|
76
|
+
5
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
id="step-10"
|
|
80
|
+
class="step-btn"
|
|
81
|
+
style="flex: 1; padding: 10px; background: ${state.step === 10 ? '#2196f3' : '#ffffff'}; border: 1px solid #e0e0e0; border-radius: 4px; color: ${state.step === 10 ? '#ffffff' : '#757575'}; font-size: 14px; cursor: pointer;">
|
|
82
|
+
10
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
id="step-100"
|
|
86
|
+
class="step-btn"
|
|
87
|
+
style="flex: 1; padding: 10px; background: ${state.step === 100 ? '#2196f3' : '#ffffff'}; border: 1px solid #e0e0e0; border-radius: 4px; color: ${state.step === 100 ? '#ffffff' : '#757575'}; font-size: 14px; cursor: pointer;">
|
|
88
|
+
100
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- History Card -->
|
|
94
|
+
${state.history.length > 0 ? html`
|
|
95
|
+
<div style="background: #ffffff; border-radius: 8px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);">
|
|
96
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
97
|
+
<h3 style="color: #424242; font-size: 14px; font-weight: 500; margin: 0;">Recent History</h3>
|
|
98
|
+
<button
|
|
99
|
+
id="clear-history"
|
|
100
|
+
style="padding: 6px 12px; background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; color: #757575; font-size: 12px; cursor: pointer;">
|
|
101
|
+
Clear
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div style="max-height: 200px; overflow-y: auto;">
|
|
106
|
+
${state.history.slice(-10).reverse().map(item => html`
|
|
107
|
+
<div style="padding: 10px 0; border-bottom: 1px solid #f5f5f5; display: flex; justify-content: space-between; align-items: center;">
|
|
108
|
+
<span style="color: #424242; font-size: 14px;">${item.action}</span>
|
|
109
|
+
<span style="color: ${item.value >= 0 ? '#4caf50' : '#f44336'}; font-size: 14px; font-weight: 500;">
|
|
110
|
+
${item.value >= 0 ? '+' : ''}${item.value}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
`).join('')}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
` : ''}
|
|
117
|
+
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const events = {
|
|
123
|
+
'#increment-btn': {
|
|
124
|
+
click: () => {
|
|
125
|
+
state.count += state.step;
|
|
126
|
+
addToHistory(state, `Increased by ${state.step}`, state.step);
|
|
127
|
+
saveState(state);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
'#decrement-btn': {
|
|
131
|
+
click: () => {
|
|
132
|
+
state.count -= state.step;
|
|
133
|
+
addToHistory(state, `Decreased by ${state.step}`, -state.step);
|
|
134
|
+
saveState(state);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
'#reset-btn': {
|
|
138
|
+
click: () => {
|
|
139
|
+
const oldCount = state.count;
|
|
140
|
+
state.count = 0;
|
|
141
|
+
addToHistory(state, 'Reset to 0', -oldCount);
|
|
142
|
+
saveState(state);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
'#step-1': {
|
|
146
|
+
click: () => {
|
|
147
|
+
state.step = 1;
|
|
148
|
+
saveState(state);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
'#step-5': {
|
|
152
|
+
click: () => {
|
|
153
|
+
state.step = 5;
|
|
154
|
+
saveState(state);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
'#step-10': {
|
|
158
|
+
click: () => {
|
|
159
|
+
state.step = 10;
|
|
160
|
+
saveState(state);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
'#step-100': {
|
|
164
|
+
click: () => {
|
|
165
|
+
state.step = 100;
|
|
166
|
+
saveState(state);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
'#clear-history': {
|
|
170
|
+
click: () => {
|
|
171
|
+
state.history = [];
|
|
172
|
+
saveState(state);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return { element, events };
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Helper functions
|
|
182
|
+
function addToHistory(state, action, value) {
|
|
183
|
+
state.history.push({
|
|
184
|
+
action,
|
|
185
|
+
value,
|
|
186
|
+
timestamp: Date.now()
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Keep only last 50 items
|
|
190
|
+
if (state.history.length > 50) {
|
|
191
|
+
state.history = state.history.slice(-50);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function saveState(state) {
|
|
196
|
+
local.set('counterState', {
|
|
197
|
+
count: state.count,
|
|
198
|
+
step: state.step,
|
|
199
|
+
history: state.history
|
|
200
|
+
});
|
|
201
|
+
}
|