@currentjs/gen 0.2.2 → 0.3.1
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/CHANGELOG.md +240 -0
- package/README.md +256 -0
- package/dist/cli.js +26 -0
- package/dist/commands/createApp.js +2 -0
- package/dist/commands/generateAll.js +153 -29
- package/dist/commands/migrateCommit.d.ts +1 -0
- package/dist/commands/migrateCommit.js +201 -0
- package/dist/generators/controllerGenerator.d.ts +7 -0
- package/dist/generators/controllerGenerator.js +60 -29
- package/dist/generators/domainModelGenerator.d.ts +7 -0
- package/dist/generators/domainModelGenerator.js +57 -3
- package/dist/generators/serviceGenerator.d.ts +16 -1
- package/dist/generators/serviceGenerator.js +125 -12
- package/dist/generators/storeGenerator.d.ts +8 -0
- package/dist/generators/storeGenerator.js +133 -7
- package/dist/generators/templateGenerator.d.ts +19 -0
- package/dist/generators/templateGenerator.js +216 -11
- package/dist/generators/templates/appTemplates.d.ts +8 -7
- package/dist/generators/templates/appTemplates.js +11 -1572
- package/dist/generators/templates/data/appTsTemplate +39 -0
- package/dist/generators/templates/data/appYamlTemplate +4 -0
- package/dist/generators/templates/data/cursorRulesTemplate +671 -0
- package/dist/generators/templates/data/errorTemplate +28 -0
- package/dist/generators/templates/data/frontendScriptTemplate +739 -0
- package/dist/generators/templates/data/mainViewTemplate +16 -0
- package/dist/generators/templates/data/translationsTemplate +68 -0
- package/dist/generators/templates/data/tsConfigTemplate +19 -0
- package/dist/generators/templates/viewTemplates.d.ts +10 -1
- package/dist/generators/templates/viewTemplates.js +138 -6
- package/dist/generators/validationGenerator.d.ts +5 -0
- package/dist/generators/validationGenerator.js +51 -0
- package/dist/utils/constants.d.ts +3 -0
- package/dist/utils/constants.js +5 -2
- package/dist/utils/generationRegistry.js +1 -1
- package/dist/utils/migrationUtils.d.ts +49 -0
- package/dist/utils/migrationUtils.js +291 -0
- package/howto.md +157 -65
- package/package.json +3 -2
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Frontend Functions for Generated Apps
|
|
3
|
+
* This script provides utilities for UI feedback, navigation, form handling, and SPA-like behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Global configuration
|
|
7
|
+
window.AppConfig = {
|
|
8
|
+
toastDuration: 3000,
|
|
9
|
+
modalDuration: 1200,
|
|
10
|
+
animationDuration: 300,
|
|
11
|
+
debounceDelay: 300,
|
|
12
|
+
translations: {},
|
|
13
|
+
currentLang: null,
|
|
14
|
+
isTranslationEnabled: false, // change to true to enable translations
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
(() => {
|
|
18
|
+
// ===== AUTHENTICATION HELPERS =====
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get authentication token from localStorage
|
|
22
|
+
* @returns {string|null} JWT token or null if not found
|
|
23
|
+
*/
|
|
24
|
+
function getAuthToken() {
|
|
25
|
+
return localStorage.getItem('authToken') || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set authentication token in localStorage
|
|
30
|
+
* @param {string} token - JWT token to store
|
|
31
|
+
*/
|
|
32
|
+
function setAuthToken(token) {
|
|
33
|
+
if (token) {
|
|
34
|
+
localStorage.setItem('authToken', token);
|
|
35
|
+
document.cookie = `authToken=${token}; max-age=31536000; path=/`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Remove authentication token from localStorage
|
|
41
|
+
*/
|
|
42
|
+
function clearAuthToken() {
|
|
43
|
+
localStorage.removeItem('authToken');
|
|
44
|
+
document.cookie = `authToken=; max-age=0; path=/`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build headers object with authentication if token exists
|
|
49
|
+
* @param {object} additionalHeaders - Additional headers to merge
|
|
50
|
+
* @returns {object} Headers object with auth header if token exists
|
|
51
|
+
*/
|
|
52
|
+
function buildAuthHeaders(additionalHeaders = {}) {
|
|
53
|
+
const headers = { ...additionalHeaders };
|
|
54
|
+
const token = getAuthToken();
|
|
55
|
+
|
|
56
|
+
if (token) {
|
|
57
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return headers;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ===== TRANSLATION FUNCTIONS =====
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get current language
|
|
67
|
+
* Priority: localStorage -> navigator.language -> 'en'
|
|
68
|
+
* @returns {string} Current language code
|
|
69
|
+
*/
|
|
70
|
+
function getCurrentLanguage() {
|
|
71
|
+
if (window.AppConfig.currentLang) {
|
|
72
|
+
return window.AppConfig.currentLang;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 1. Check localStorage
|
|
76
|
+
const storedLang = localStorage.getItem('lang');
|
|
77
|
+
if (storedLang) {
|
|
78
|
+
window.AppConfig.currentLang = storedLang;
|
|
79
|
+
return storedLang;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Check browser language (Accept-Language equivalent)
|
|
83
|
+
const browserLang = navigator.language || navigator.languages?.[0];
|
|
84
|
+
if (browserLang) {
|
|
85
|
+
// Extract language code (e.g., 'en-US' -> 'en')
|
|
86
|
+
const langCode = browserLang.split('-')[0];
|
|
87
|
+
window.AppConfig.currentLang = langCode;
|
|
88
|
+
return langCode;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Default fallback
|
|
92
|
+
window.AppConfig.currentLang = 'en';
|
|
93
|
+
return 'en';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Translate string to current language
|
|
98
|
+
* @param {string} str - String in default language to translate
|
|
99
|
+
* @returns {string} Translated string or original if translation not found
|
|
100
|
+
*/
|
|
101
|
+
function t(str) {
|
|
102
|
+
if (!str || typeof str !== 'string') return str;
|
|
103
|
+
|
|
104
|
+
const currentLang = getCurrentLanguage();
|
|
105
|
+
|
|
106
|
+
// If current language is the default or no translations loaded, return original
|
|
107
|
+
if (currentLang === 'en' || !window.AppConfig.translations || !window.AppConfig.translations[currentLang]) {
|
|
108
|
+
return str;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const translation = window.AppConfig.translations[currentLang][str];
|
|
112
|
+
return translation || str;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set current language and save to localStorage
|
|
117
|
+
* @param {string} langKey - Language code (e.g., 'en', 'ru', 'pl')
|
|
118
|
+
*/
|
|
119
|
+
function setLang(langKey) {
|
|
120
|
+
if (!langKey || typeof langKey !== 'string') return;
|
|
121
|
+
|
|
122
|
+
window.AppConfig.currentLang = langKey;
|
|
123
|
+
localStorage.setItem('lang', langKey);
|
|
124
|
+
|
|
125
|
+
// Optionally reload page to apply translations
|
|
126
|
+
// Uncomment the next line if you want automatic page reload on language change
|
|
127
|
+
// window.location.reload();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Load translations from JSON file
|
|
132
|
+
* @param {string} url - URL to translations JSON file (default: '/translations.json')
|
|
133
|
+
*/
|
|
134
|
+
function loadTranslations(url = '/translations.json') {
|
|
135
|
+
if (!window.AppConfig.isTranslationEnabled) return;
|
|
136
|
+
fetch(url)
|
|
137
|
+
.then(response => {
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
console.warn('Translations file not found:', url);
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
return response.json();
|
|
143
|
+
})
|
|
144
|
+
.then(translations => {
|
|
145
|
+
window.AppConfig.translations = translations || {};
|
|
146
|
+
})
|
|
147
|
+
.catch(error => {
|
|
148
|
+
console.warn('Failed to load translations:', error);
|
|
149
|
+
window.AppConfig.translations = {};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===== UI FEEDBACK & NOTIFICATIONS =====
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Show a toast notification
|
|
157
|
+
* @param {string} message - The message to display
|
|
158
|
+
* @param {string} type - 'success', 'error', 'info', 'warning'
|
|
159
|
+
*/
|
|
160
|
+
function showToast(message, type = 'info') {
|
|
161
|
+
// Translate the message
|
|
162
|
+
message = t(message);
|
|
163
|
+
const toast = document.createElement('div');
|
|
164
|
+
toast.className = 'app-toast app-toast-' + type;
|
|
165
|
+
toast.textContent = message;
|
|
166
|
+
toast.style.cssText = `
|
|
167
|
+
position: fixed;
|
|
168
|
+
top: 20px;
|
|
169
|
+
right: 20px;
|
|
170
|
+
padding: 12px 24px;
|
|
171
|
+
border-radius: 4px;
|
|
172
|
+
color: white;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
z-index: 10000;
|
|
175
|
+
max-width: 300px;
|
|
176
|
+
word-wrap: break-word;
|
|
177
|
+
transition: all \${window.AppConfig.animationDuration}ms ease;
|
|
178
|
+
transform: translateX(100%);
|
|
179
|
+
opacity: 0;
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
// Type-specific styling
|
|
183
|
+
const colors = {
|
|
184
|
+
success: '#10b981',
|
|
185
|
+
error: '#ef4444',
|
|
186
|
+
warning: '#f59e0b',
|
|
187
|
+
info: '#3b82f6'
|
|
188
|
+
};
|
|
189
|
+
toast.style.backgroundColor = colors[type] || colors.info;
|
|
190
|
+
|
|
191
|
+
document.body.appendChild(toast);
|
|
192
|
+
|
|
193
|
+
// Animate in
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
toast.style.transform = 'translateX(0)';
|
|
196
|
+
toast.style.opacity = '1';
|
|
197
|
+
}, 10);
|
|
198
|
+
|
|
199
|
+
// Auto remove
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
toast.style.transform = 'translateX(100%)';
|
|
202
|
+
toast.style.opacity = '0';
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (toast.parentNode) {
|
|
205
|
+
toast.parentNode.removeChild(toast);
|
|
206
|
+
}
|
|
207
|
+
}, window.AppConfig.animationDuration);
|
|
208
|
+
}, window.AppConfig.toastDuration);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Display inline message in specific container
|
|
213
|
+
* @param {string} elementId - ID of the target element
|
|
214
|
+
* @param {string} message - The message to display
|
|
215
|
+
* @param {string} type - 'success', 'error', 'info', 'warning'
|
|
216
|
+
*/
|
|
217
|
+
function showMessage(elementId, message, type = 'info') {
|
|
218
|
+
const element = getElementSafely('#' + elementId);
|
|
219
|
+
if (!element) return;
|
|
220
|
+
|
|
221
|
+
// Translate the message
|
|
222
|
+
message = t(message);
|
|
223
|
+
element.textContent = message;
|
|
224
|
+
element.className = 'app-message app-message-' + type;
|
|
225
|
+
element.style.cssText = `
|
|
226
|
+
padding: 8px 12px;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
margin: 8px 0;
|
|
229
|
+
font-size: 14px;
|
|
230
|
+
display: block;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
// Type-specific styling
|
|
234
|
+
const styles = {
|
|
235
|
+
success: 'background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0;',
|
|
236
|
+
error: 'background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5;',
|
|
237
|
+
warning: 'background: #fef3c7; color: #92400e; border: 1px solid #fcd34d;',
|
|
238
|
+
info: 'background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd;'
|
|
239
|
+
};
|
|
240
|
+
element.style.cssText += styles[type] || styles.info;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Show modal dialog
|
|
245
|
+
* @param {string} modalId - ID of the modal element
|
|
246
|
+
* @param {string} message - The message to display
|
|
247
|
+
* @param {string} type - 'success', 'error', 'info', 'warning'
|
|
248
|
+
*/
|
|
249
|
+
function showModal(modalId, message, type = 'info') {
|
|
250
|
+
let modal = getElementSafely('#' + modalId);
|
|
251
|
+
|
|
252
|
+
if (!modal) {
|
|
253
|
+
// Create modal if it doesn't exist
|
|
254
|
+
modal = document.createElement('dialog');
|
|
255
|
+
modal.id = modalId;
|
|
256
|
+
modal.innerHTML = `
|
|
257
|
+
<div class="modal-content">
|
|
258
|
+
<div class="modal-header">
|
|
259
|
+
<button class="modal-close" onclick="this.closest('dialog').close()">×</button>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="modal-body"></div>
|
|
262
|
+
</div>
|
|
263
|
+
`;
|
|
264
|
+
modal.style.cssText = `
|
|
265
|
+
border: none;
|
|
266
|
+
border-radius: 8px;
|
|
267
|
+
padding: 0;
|
|
268
|
+
max-width: 400px;
|
|
269
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
270
|
+
`;
|
|
271
|
+
document.body.appendChild(modal);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const content = modal.querySelector('.modal-body');
|
|
275
|
+
if (content) {
|
|
276
|
+
// Translate the message
|
|
277
|
+
message = t(message);
|
|
278
|
+
content.textContent = message;
|
|
279
|
+
content.className = 'modal-body modal-' + type;
|
|
280
|
+
content.style.cssText = 'padding: 20px; text-align: center;';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (modal.showModal) {
|
|
284
|
+
modal.showModal();
|
|
285
|
+
setTimeout(() => {
|
|
286
|
+
try { modal.close(); } catch(e) {}
|
|
287
|
+
}, window.AppConfig.modalDuration);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ===== FORM & CONTENT MANAGEMENT =====
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Safe element removal with animation
|
|
295
|
+
* @param {string} selector - CSS selector for element to remove
|
|
296
|
+
*/
|
|
297
|
+
function removeElement(selector) {
|
|
298
|
+
const element = getElementSafely(selector);
|
|
299
|
+
if (!element) return;
|
|
300
|
+
|
|
301
|
+
element.style.transition = `opacity ${window.AppConfig.animationDuration}ms ease`;
|
|
302
|
+
element.style.opacity = '0';
|
|
303
|
+
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
if (element.parentNode) {
|
|
306
|
+
element.parentNode.removeChild(element);
|
|
307
|
+
}
|
|
308
|
+
}, window.AppConfig.animationDuration);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ===== CUSTOM SPA INTEGRATION =====
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Centralized success handler that executes strategy array
|
|
315
|
+
* @param {object} response - The response object
|
|
316
|
+
* @param {string[]} strategy - Array of strategy actions
|
|
317
|
+
* @param {object} options - Additional options (basePath, entityName, etc.)
|
|
318
|
+
*/
|
|
319
|
+
function handleFormSuccess(response, strategy = ['back', 'toast'], options = {}) {
|
|
320
|
+
const { basePath, entityName = 'Item' } = options;
|
|
321
|
+
|
|
322
|
+
strategy.forEach(action => {
|
|
323
|
+
switch (action) {
|
|
324
|
+
case 'toast':
|
|
325
|
+
showToast(`${entityName} saved successfully`, 'success');
|
|
326
|
+
break;
|
|
327
|
+
case 'message':
|
|
328
|
+
if (options.messageId) {
|
|
329
|
+
showMessage(options.messageId, `${entityName} saved successfully`, 'success');
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
case 'modal':
|
|
333
|
+
if (options.modalId) {
|
|
334
|
+
showModal(options.modalId, `${entityName} saved successfully`, 'success');
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case 'remove':
|
|
338
|
+
// If targetSelector is specified, remove that element instead of the form
|
|
339
|
+
if (options.targetSelector) {
|
|
340
|
+
removeElement(options.targetSelector);
|
|
341
|
+
} else if (options.formSelector) {
|
|
342
|
+
removeElement(options.formSelector);
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
case 'redirect':
|
|
346
|
+
if (basePath) {
|
|
347
|
+
if (basePath.startsWith('/') || basePath.startsWith('http')) {
|
|
348
|
+
navigateToPage(basePath);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case 'back':
|
|
353
|
+
if (window.history.length > 1) {
|
|
354
|
+
window.history.back();
|
|
355
|
+
} else {
|
|
356
|
+
window.location.href = '/';
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
case 'reload':
|
|
360
|
+
case 'refresh':
|
|
361
|
+
showToast(t('Reloading...'), 'info');
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
window.location.reload();
|
|
364
|
+
}, 500);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Handle internal link navigation via fetch
|
|
372
|
+
* @param {string} url - The URL to navigate to
|
|
373
|
+
* @param {Element} targetElement - Element to update with new content (default: #main)
|
|
374
|
+
*/
|
|
375
|
+
function navigateToPage(url, targetElement = null) {
|
|
376
|
+
const target = targetElement || document.querySelector('#main');
|
|
377
|
+
if (!target) return;
|
|
378
|
+
|
|
379
|
+
showLoading('#main');
|
|
380
|
+
|
|
381
|
+
fetch(url, {
|
|
382
|
+
headers: buildAuthHeaders({
|
|
383
|
+
'Accept': 'text/html',
|
|
384
|
+
'X-Partial-Content': 'true'
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
.then(response => {
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
390
|
+
}
|
|
391
|
+
return response.text();
|
|
392
|
+
})
|
|
393
|
+
.then(html => {
|
|
394
|
+
target.innerHTML = html;
|
|
395
|
+
// Update browser history
|
|
396
|
+
window.history.pushState({}, '', url);
|
|
397
|
+
// Re-initialize event listeners for new content
|
|
398
|
+
initializeEventListeners();
|
|
399
|
+
})
|
|
400
|
+
.catch(error => {
|
|
401
|
+
console.error('Navigation failed:', error);
|
|
402
|
+
showToast('Failed to load page', 'error');
|
|
403
|
+
// Fallback to normal navigation
|
|
404
|
+
window.location.href = url;
|
|
405
|
+
})
|
|
406
|
+
.finally(() => {
|
|
407
|
+
hideLoading('#main');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Convert form value to appropriate type based on field type
|
|
413
|
+
* @param {string} value - Raw form value
|
|
414
|
+
* @param {string} fieldType - Field type (number, boolean, etc.)
|
|
415
|
+
* @returns {any} Converted value
|
|
416
|
+
*/
|
|
417
|
+
function convertFieldValue(value, fieldType) {
|
|
418
|
+
if (!value || value === '') {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
switch (fieldType.toLowerCase()) {
|
|
423
|
+
case 'number':
|
|
424
|
+
case 'int':
|
|
425
|
+
case 'integer':
|
|
426
|
+
const intVal = parseInt(value, 10);
|
|
427
|
+
return isNaN(intVal) ? null : intVal;
|
|
428
|
+
|
|
429
|
+
case 'float':
|
|
430
|
+
case 'decimal':
|
|
431
|
+
const floatVal = parseFloat(value);
|
|
432
|
+
return isNaN(floatVal) ? null : floatVal;
|
|
433
|
+
|
|
434
|
+
case 'boolean':
|
|
435
|
+
case 'bool':
|
|
436
|
+
if (value === 'true') return true;
|
|
437
|
+
if (value === 'false') return false;
|
|
438
|
+
return Boolean(value);
|
|
439
|
+
|
|
440
|
+
case 'enum':
|
|
441
|
+
case 'string':
|
|
442
|
+
case 'text':
|
|
443
|
+
default:
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Handle form submission via fetch with JSON data
|
|
450
|
+
* @param {HTMLFormElement} form - The form element
|
|
451
|
+
* @param {string[]} strategy - Strategy actions to execute on success
|
|
452
|
+
* @param {object} options - Additional options
|
|
453
|
+
*/
|
|
454
|
+
function submitForm(form, strategy = ['back', 'toast'], options = {}) {
|
|
455
|
+
const formData = new FormData(form);
|
|
456
|
+
const jsonData = {};
|
|
457
|
+
|
|
458
|
+
// Get field types from form data attribute
|
|
459
|
+
const fieldTypesAttr = form.getAttribute('data-field-types');
|
|
460
|
+
const fieldTypes = fieldTypesAttr ? JSON.parse(fieldTypesAttr) : {};
|
|
461
|
+
|
|
462
|
+
// Convert FormData to JSON with proper typing
|
|
463
|
+
for (const [key, value] of formData.entries()) {
|
|
464
|
+
const fieldType = fieldTypes[key] || 'string';
|
|
465
|
+
const convertedValue = convertFieldValue(value, fieldType);
|
|
466
|
+
|
|
467
|
+
if (jsonData[key]) {
|
|
468
|
+
// Handle multiple values (e.g., checkboxes)
|
|
469
|
+
if (Array.isArray(jsonData[key])) {
|
|
470
|
+
jsonData[key].push(convertedValue);
|
|
471
|
+
} else {
|
|
472
|
+
jsonData[key] = [jsonData[key], convertedValue];
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
jsonData[key] = convertedValue;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const url = form.getAttribute('data-action') || form.action;
|
|
480
|
+
const method = (form.getAttribute('data-method') || form.method || 'POST').toUpperCase();
|
|
481
|
+
|
|
482
|
+
showLoading(form);
|
|
483
|
+
|
|
484
|
+
fetch(url, {
|
|
485
|
+
method,
|
|
486
|
+
headers: buildAuthHeaders({
|
|
487
|
+
'Content-Type': 'application/json',
|
|
488
|
+
'Accept': 'application/json'
|
|
489
|
+
}),
|
|
490
|
+
body: JSON.stringify(jsonData)
|
|
491
|
+
})
|
|
492
|
+
.then(response => {
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
495
|
+
}
|
|
496
|
+
return response.json();
|
|
497
|
+
})
|
|
498
|
+
.then(data => {
|
|
499
|
+
handleFormSuccess(data, strategy, options);
|
|
500
|
+
})
|
|
501
|
+
.catch(error => {
|
|
502
|
+
console.error('Form submission failed:', error);
|
|
503
|
+
handleFormError({ message: error.message || 'Form submission failed' }, options);
|
|
504
|
+
})
|
|
505
|
+
.finally(() => {
|
|
506
|
+
hideLoading(form);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Standardized error handling for forms
|
|
512
|
+
* @param {object} response - The error response
|
|
513
|
+
* @param {object} options - Options including target elements
|
|
514
|
+
*/
|
|
515
|
+
function handleFormError(response, options = {}) {
|
|
516
|
+
const message = response.message || 'An error occurred';
|
|
517
|
+
|
|
518
|
+
if (options.messageId) {
|
|
519
|
+
showMessage(options.messageId, message, 'error');
|
|
520
|
+
} else {
|
|
521
|
+
showToast(message, 'error');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ===== UTILITY FUNCTIONS =====
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Safe element selection with error handling
|
|
529
|
+
* @param {string} selector - CSS selector
|
|
530
|
+
* @returns {Element|null}
|
|
531
|
+
*/
|
|
532
|
+
function getElementSafely(selector) {
|
|
533
|
+
try {
|
|
534
|
+
return document.querySelector(selector);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
console.warn('Invalid selector:', selector);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Debounce utility for search/input handlers
|
|
543
|
+
* @param {Function} fn - Function to debounce
|
|
544
|
+
* @param {number} delay - Delay in milliseconds
|
|
545
|
+
* @returns {Function}
|
|
546
|
+
*/
|
|
547
|
+
function debounce(fn, delay = window.AppConfig.debounceDelay) {
|
|
548
|
+
let timeoutId;
|
|
549
|
+
return function(...args) {
|
|
550
|
+
clearTimeout(timeoutId);
|
|
551
|
+
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Show loading state for target element
|
|
557
|
+
* @param {string} target - CSS selector for target element
|
|
558
|
+
*/
|
|
559
|
+
function showLoading(target) {
|
|
560
|
+
const element = getElementSafely(target);
|
|
561
|
+
if (!element) return;
|
|
562
|
+
|
|
563
|
+
element.style.position = 'relative';
|
|
564
|
+
element.style.pointerEvents = 'none';
|
|
565
|
+
element.style.opacity = '0.6';
|
|
566
|
+
|
|
567
|
+
const loader = document.createElement('div');
|
|
568
|
+
loader.className = 'app-loader';
|
|
569
|
+
loader.style.cssText = `
|
|
570
|
+
position: absolute;
|
|
571
|
+
top: 50%;
|
|
572
|
+
left: 50%;
|
|
573
|
+
transform: translate(-50%, -50%);
|
|
574
|
+
width: 20px;
|
|
575
|
+
height: 20px;
|
|
576
|
+
border: 2px solid #f3f3f3;
|
|
577
|
+
border-top: 2px solid #3498db;
|
|
578
|
+
border-radius: 50%;
|
|
579
|
+
animation: spin 1s linear infinite;
|
|
580
|
+
z-index: 1000;
|
|
581
|
+
`;
|
|
582
|
+
|
|
583
|
+
element.appendChild(loader);
|
|
584
|
+
|
|
585
|
+
// Add CSS animation if not exists
|
|
586
|
+
if (!document.querySelector('#app-loader-styles')) {
|
|
587
|
+
const style = document.createElement('style');
|
|
588
|
+
style.id = 'app-loader-styles';
|
|
589
|
+
style.textContent = `
|
|
590
|
+
@keyframes spin {
|
|
591
|
+
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
592
|
+
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
593
|
+
}
|
|
594
|
+
`;
|
|
595
|
+
document.head.appendChild(style);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Hide loading state for target element
|
|
601
|
+
* @param {string} target - CSS selector for target element
|
|
602
|
+
*/
|
|
603
|
+
function hideLoading(target) {
|
|
604
|
+
const element = getElementSafely(target);
|
|
605
|
+
if (!element) return;
|
|
606
|
+
|
|
607
|
+
element.style.pointerEvents = '';
|
|
608
|
+
element.style.opacity = '';
|
|
609
|
+
|
|
610
|
+
const loader = element.querySelector('.app-loader');
|
|
611
|
+
if (loader) {
|
|
612
|
+
loader.remove();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ===== INITIALIZATION =====
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Initialize event listeners for links and forms
|
|
620
|
+
*/
|
|
621
|
+
function initializeEventListeners() {
|
|
622
|
+
// Handle internal links
|
|
623
|
+
document.querySelectorAll('a[href]').forEach(link => {
|
|
624
|
+
const href = link.getAttribute('href');
|
|
625
|
+
|
|
626
|
+
// Skip external links, anchors, and special protocols
|
|
627
|
+
if (!href ||
|
|
628
|
+
href.startsWith('http://') ||
|
|
629
|
+
href.startsWith('https://') ||
|
|
630
|
+
href.startsWith('mailto:') ||
|
|
631
|
+
href.startsWith('tel:') ||
|
|
632
|
+
href.startsWith('#') ||
|
|
633
|
+
href.startsWith('javascript:')) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Remove any existing event listeners and add new one
|
|
638
|
+
link.removeEventListener('click', handleLinkClick);
|
|
639
|
+
link.addEventListener('click', handleLinkClick);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Handle forms with strategy
|
|
643
|
+
document.querySelectorAll('form[data-strategy]').forEach(form => {
|
|
644
|
+
// Remove any existing event listeners and add new one
|
|
645
|
+
form.removeEventListener('submit', handleFormSubmit);
|
|
646
|
+
form.addEventListener('submit', handleFormSubmit);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Handle link click for internal navigation
|
|
652
|
+
* @param {Event} event - Click event
|
|
653
|
+
*/
|
|
654
|
+
function handleLinkClick(event) {
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
const href = event.currentTarget.getAttribute('href');
|
|
657
|
+
if (href) {
|
|
658
|
+
navigateToPage(href);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Handle form submission
|
|
664
|
+
* @param {Event} event - Submit event
|
|
665
|
+
*/
|
|
666
|
+
function handleFormSubmit(event) {
|
|
667
|
+
event.preventDefault();
|
|
668
|
+
const form = event.target;
|
|
669
|
+
|
|
670
|
+
// Check for confirmation message
|
|
671
|
+
const confirmMessage = form.getAttribute('data-confirm-message');
|
|
672
|
+
if (confirmMessage) {
|
|
673
|
+
const confirmed = confirm(t(confirmMessage));
|
|
674
|
+
if (!confirmed) {
|
|
675
|
+
return; // User cancelled
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const strategyAttr = form.getAttribute('data-strategy');
|
|
680
|
+
const strategy = strategyAttr ? JSON.parse(strategyAttr) : ['back', 'toast'];
|
|
681
|
+
|
|
682
|
+
// Extract options from form data attributes
|
|
683
|
+
const options = {
|
|
684
|
+
basePath: form.getAttribute('data-base-path') || '',
|
|
685
|
+
entityName: form.getAttribute('data-entity-name') || 'Item',
|
|
686
|
+
messageId: form.getAttribute('data-message-id'),
|
|
687
|
+
modalId: form.getAttribute('data-modal-id'),
|
|
688
|
+
formSelector: `form[data-template="${form.getAttribute('data-template')}"]`,
|
|
689
|
+
targetSelector: form.getAttribute('data-target-selector')
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
submitForm(form, strategy, options);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Set up event handlers on page load
|
|
696
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
697
|
+
// Initialize event listeners
|
|
698
|
+
initializeEventListeners();
|
|
699
|
+
|
|
700
|
+
// Handle browser back/forward buttons
|
|
701
|
+
window.addEventListener('popstate', function(event) {
|
|
702
|
+
// Reload the page content for the current URL
|
|
703
|
+
navigateToPage(window.location.pathname);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// Load translations on page load
|
|
707
|
+
loadTranslations();
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Expose functions globally
|
|
711
|
+
window.App = {
|
|
712
|
+
auth: {
|
|
713
|
+
setAuthToken,
|
|
714
|
+
clearAuthToken,
|
|
715
|
+
buildAuthHeaders,
|
|
716
|
+
},
|
|
717
|
+
lang: {
|
|
718
|
+
t,
|
|
719
|
+
set: setLang,
|
|
720
|
+
get: getCurrentLanguage,
|
|
721
|
+
},
|
|
722
|
+
ui: {
|
|
723
|
+
showToast,
|
|
724
|
+
showMessage,
|
|
725
|
+
showModal,
|
|
726
|
+
showLoading,
|
|
727
|
+
hideLoading,
|
|
728
|
+
},
|
|
729
|
+
nav: {
|
|
730
|
+
go: navigateToPage,
|
|
731
|
+
submit: submitForm,
|
|
732
|
+
},
|
|
733
|
+
utils: {
|
|
734
|
+
$: getElementSafely,
|
|
735
|
+
debounce,
|
|
736
|
+
initializeEventListeners,
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
})();
|