@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.
@@ -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
+ }