@hackthedev/dsync-shop 1.0.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/web/script.js ADDED
@@ -0,0 +1,605 @@
1
+ const config = {
2
+ basePath: '/shop',
3
+ apiEndpoints: {
4
+ products: '/products/list',
5
+ productsByCategory: '/products/list/',
6
+ categories: '/categories/list',
7
+ actions: '/actions/list',
8
+ product: '/product/',
9
+ productCreate: '/product/create',
10
+ productUpdate: '/product/update/',
11
+ productDelete: '/product/delete/',
12
+ categoryCreate: '/category/create',
13
+ categoryUpdate: '/category/update/',
14
+ categoryDelete: '/category/delete/',
15
+ adminCheck: '/admin/check'
16
+ }
17
+ };
18
+
19
+ let currentPurchase = null;
20
+ let allProducts = [];
21
+ let allCategories = [];
22
+ let allActions = [];
23
+ let currentCategory = 'all';
24
+ let currentForm = null;
25
+ let adminProductSearch = '';
26
+
27
+ function authHeaders() {
28
+ return {
29
+ 'Content-Type': 'application/json',
30
+ 'x-user-id': localStorage.getItem('id'),
31
+ 'x-token': localStorage.getItem('token')
32
+ };
33
+ }
34
+
35
+ async function checkAdminStatus() {
36
+ try {
37
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.adminCheck}`, {
38
+ headers: authHeaders()
39
+ });
40
+ const data = await response.json();
41
+ const btn = document.getElementById('adminBtn');
42
+ if (btn) btn.style.display = data.isAdmin ? 'block' : 'none';
43
+ } catch (e) {
44
+ const btn = document.getElementById('adminBtn');
45
+ if (btn) btn.style.display = 'none';
46
+ }
47
+ }
48
+
49
+ async function fetchCategories() {
50
+ try {
51
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.categories}`);
52
+ const data = await response.json();
53
+ return data.error ? [] : (data.categories || []);
54
+ } catch (e) {
55
+ console.error('error fetching categories:', e);
56
+ return [];
57
+ }
58
+ }
59
+
60
+ async function fetchProducts(category = null) {
61
+ try {
62
+ const url = category
63
+ ? `${config.basePath}${config.apiEndpoints.productsByCategory}${category}`
64
+ : `${config.basePath}${config.apiEndpoints.products}`;
65
+ const response = await fetch(url);
66
+ const data = await response.json();
67
+ return data.error ? [] : (data.products || []);
68
+ } catch (e) {
69
+ console.error('error fetching products:', e);
70
+ return [];
71
+ }
72
+ }
73
+
74
+ async function fetchActions() {
75
+ try {
76
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.actions}`, {
77
+ headers: authHeaders()
78
+ });
79
+ const data = await response.json();
80
+ return data.error ? [] : (data.actions || []);
81
+ } catch (e) {
82
+ console.error('error fetching actions:', e);
83
+ return [];
84
+ }
85
+ }
86
+
87
+ function displayCategories(categories) {
88
+ const categoryNav = document.getElementById('categoryNav');
89
+ categoryNav.innerHTML = '';
90
+
91
+ const allBtn = document.createElement('button');
92
+ allBtn.className = 'category-btn active';
93
+ allBtn.dataset.category = 'all';
94
+ allBtn.textContent = 'all products';
95
+ allBtn.addEventListener('click', () => filterByCategory('all'));
96
+ categoryNav.appendChild(allBtn);
97
+
98
+ categories.forEach(category => {
99
+ const btn = document.createElement('button');
100
+ btn.className = 'category-btn';
101
+ btn.dataset.category = category.name;
102
+ btn.textContent = category.name;
103
+ btn.addEventListener('click', () => filterByCategory(category.name));
104
+ categoryNav.appendChild(btn);
105
+ });
106
+ }
107
+
108
+ function displayProducts(products) {
109
+ const container = document.getElementById('productsContainer');
110
+ if (products.length === 0) {
111
+ container.innerHTML = '<div class="error">no products found</div>';
112
+ return;
113
+ }
114
+ container.innerHTML = '';
115
+ products.forEach(product => {
116
+ const productEl = document.createElement('div');
117
+ productEl.className = 'product';
118
+ productEl.innerHTML = `
119
+ <div class="product-image">
120
+ <img src="${product.image_url || 'https://via.placeholder.com/300x200/5865f2/ffffff?text=Product'}" alt="${product.name}">
121
+ </div>
122
+ <div class="product-info">
123
+ <div class="product-title">${product.name}</div>
124
+ <div class="product-description">${product.description || ''}</div>
125
+ <div class="product-footer">
126
+ <div class="product-price">$${parseFloat(product.price).toFixed(2)}</div>
127
+ <div class="payment-buttons">
128
+ <button class="btn-payment btn-paypal" data-payment="paypal">paypal</button>
129
+ <button class="btn-payment btn-crypto" data-payment="crypto">crypto</button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ `;
134
+ productEl.querySelectorAll('.btn-payment').forEach(btn => {
135
+ btn.addEventListener('click', () => handlePaymentClick(product, btn.dataset.payment));
136
+ });
137
+ container.appendChild(productEl);
138
+ });
139
+ }
140
+
141
+ async function filterByCategory(categoryName) {
142
+ currentCategory = categoryName;
143
+ document.querySelectorAll('.category-btn').forEach(btn => {
144
+ btn.classList.toggle('active', btn.dataset.category === categoryName);
145
+ });
146
+ const container = document.getElementById('productsContainer');
147
+ container.innerHTML = '<div class="loading">loading products...</div>';
148
+ const products = categoryName === 'all' ? await fetchProducts() : await fetchProducts(categoryName);
149
+ displayProducts(products);
150
+ }
151
+
152
+ function handlePaymentClick(product, paymentMethod) {
153
+ currentPurchase = {id: product.id, name: product.name, price: product.price, payment: paymentMethod};
154
+ document.getElementById('modalProduct').textContent = product.name;
155
+ document.getElementById('modalPrice').textContent = `$${parseFloat(product.price).toFixed(2)}`;
156
+ document.getElementById('modalPayment').textContent = paymentMethod === 'paypal' ? 'paypal' : 'cryptocurrency';
157
+ document.getElementById('paymentModal').classList.add('show');
158
+ }
159
+
160
+ function closeModal() {
161
+ document.getElementById('paymentModal').classList.remove('show');
162
+ currentPurchase = null;
163
+ }
164
+
165
+ async function processPurchase() {
166
+ if (!currentPurchase) return;
167
+ try {
168
+ const response = await fetch(`${config.basePath}/payment/create`, {
169
+ method: 'POST',
170
+ headers: authHeaders(),
171
+ body: JSON.stringify({
172
+ product_id: currentPurchase.id,
173
+ payment_method: currentPurchase.payment
174
+ })
175
+ });
176
+
177
+ const result = await response.json();
178
+
179
+ if (result.error) {
180
+ alert('error: ' + result.error);
181
+ } else {
182
+ const paymentUrl = currentPurchase.payment === 'paypal' ? result.approvalUrl : result.hostedUrl;
183
+ const popup = window.open(paymentUrl, 'payment', 'width=600,height=800,left=100,top=100');
184
+ const checkClosed = setInterval(() => {
185
+ if (popup.closed) {
186
+ clearInterval(checkClosed);
187
+ console.log('payment window closed');
188
+ }
189
+ }, 1000);
190
+ }
191
+ } catch (e) {
192
+ alert('error: ' + e.message);
193
+ }
194
+ closeModal();
195
+ }
196
+
197
+ function openAdminPanel() {
198
+ document.getElementById('adminPanel').classList.add('show');
199
+ document.getElementById('adminOverlay').classList.add('show');
200
+ loadAdminData();
201
+ }
202
+
203
+ function closeAdminPanel() {
204
+ document.getElementById('adminPanel').classList.remove('show');
205
+ document.getElementById('adminOverlay').classList.remove('show');
206
+ }
207
+
208
+ function switchTab(tab, e) {
209
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
210
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
211
+ e.target.classList.add('active');
212
+ document.getElementById(tab + 'Tab').classList.add('active');
213
+ }
214
+
215
+ async function loadAdminData() {
216
+ await loadAdminProducts();
217
+ await loadAdminCategories();
218
+ }
219
+
220
+ function filterAdminProducts(products) {
221
+ if (!adminProductSearch) return products;
222
+ const q = adminProductSearch.toLowerCase();
223
+ return products.filter(p =>
224
+ p.name.toLowerCase().includes(q) ||
225
+ (p.description && p.description.toLowerCase().includes(q)) ||
226
+ (p.category_name && p.category_name.toLowerCase().includes(q))
227
+ );
228
+ }
229
+
230
+ async function loadAdminProducts() {
231
+ const products = await fetchProducts();
232
+ allProducts = products;
233
+ renderAdminProducts(filterAdminProducts(products));
234
+ }
235
+
236
+ function renderAdminProducts(products) {
237
+ const list = document.getElementById('productsList');
238
+
239
+ if (products.length === 0) {
240
+ list.innerHTML = '<div class="admin-empty">no products found</div>';
241
+ return;
242
+ }
243
+
244
+ list.innerHTML = '';
245
+ products.forEach(product => {
246
+ const actionLabel = product.action
247
+ ? (allActions.find(a => a.key === product.action)?.label || product.action)
248
+ : null;
249
+
250
+ let paramsText = '';
251
+ if (product.action_params) {
252
+ try {
253
+ const p = JSON.parse(product.action_params);
254
+ paramsText = Object.entries(p).map(([k, v]) => `${k}: ${v}`).join(', ');
255
+ } catch (e) {
256
+ paramsText = product.action_params;
257
+ }
258
+ }
259
+
260
+ const item = document.createElement('div');
261
+ item.className = 'admin-item';
262
+ item.innerHTML = `
263
+ <div class="admin-item-thumb">
264
+ <img src="${product.image_url || 'https://via.placeholder.com/56x56/1a1a1a/555?text=?'}" alt="${product.name}">
265
+ </div>
266
+ <div class="admin-item-info">
267
+ <div class="admin-item-header">
268
+ <h3>${product.name}</h3>
269
+ <div class="admin-item-badges">
270
+ <span class="badge badge-price">$${parseFloat(product.price).toFixed(2)}</span>
271
+ <span class="badge ${product.active ? 'badge-active' : 'badge-inactive'}">${product.active ? 'active' : 'inactive'}</span>
272
+ </div>
273
+ </div>
274
+ <div class="admin-item-meta">
275
+ ${product.category_name ? `<span class="meta-tag"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>${product.category_name}</span>` : ''}
276
+ <span class="meta-tag"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>stock: ${product.stock}</span>
277
+ ${actionLabel ? `<span class="meta-tag meta-action"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>${actionLabel}${paramsText ? ` &middot; ${paramsText}` : ''}</span>` : ''}
278
+ </div>
279
+ </div>
280
+ <div class="admin-item-actions">
281
+ <button class="btn-edit" onclick="editProduct(${product.id})">edit</button>
282
+ <button class="btn-delete" onclick="deleteProduct(${product.id})">delete</button>
283
+ </div>
284
+ `;
285
+ list.appendChild(item);
286
+ });
287
+ }
288
+
289
+ async function loadAdminCategories() {
290
+ const categories = await fetchCategories();
291
+ const list = document.getElementById('categoriesList');
292
+ if (categories.length === 0) {
293
+ list.innerHTML = '<div class="admin-empty">no categories yet</div>';
294
+ return;
295
+ }
296
+ list.innerHTML = '';
297
+ categories.forEach(category => {
298
+ const item = document.createElement('div');
299
+ item.className = 'admin-item';
300
+ item.innerHTML = `
301
+ <div class="admin-item-icon">
302
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
303
+ </div>
304
+ <div class="admin-item-info">
305
+ <div class="admin-item-header">
306
+ <h3>${category.name}</h3>
307
+ </div>
308
+ <div class="admin-item-meta">
309
+ <span class="meta-tag">${category.description || 'no description'}</span>
310
+ </div>
311
+ </div>
312
+ <div class="admin-item-actions">
313
+ <button class="btn-edit" onclick="editCategory(${category.id})">edit</button>
314
+ <button class="btn-delete" onclick="deleteCategory(${category.id})">delete</button>
315
+ </div>
316
+ `;
317
+ list.appendChild(item);
318
+ });
319
+ }
320
+
321
+ function buildActionParamFields(actionKey, existingParams = {}) {
322
+ const action = allActions.find(a => a.key === actionKey);
323
+ if (!action || !action.params || action.params.length === 0) {
324
+ return '<div class="action-params-none">no params for this action</div>';
325
+ }
326
+ return action.params.map(param => `
327
+ <div class="action-param-field form-group">
328
+ <label>${param.label}</label>
329
+ <input
330
+ type="${param.type || 'text'}"
331
+ id="actionParam_${param.key}"
332
+ value="${existingParams[param.key] !== undefined ? existingParams[param.key] : ''}"
333
+ placeholder="${param.label}"
334
+ >
335
+ </div>
336
+ `).join('');
337
+ }
338
+
339
+ function onActionChange(selectEl, existingParams = {}) {
340
+ const key = selectEl.value;
341
+ const wrapper = document.getElementById('actionParamsWrapper');
342
+ wrapper.innerHTML = key ? buildActionParamFields(key, existingParams) : '<div class="action-params-none">select an action first</div>';
343
+ }
344
+
345
+ function openProductForm(product = null) {
346
+ closeAdminPanel();
347
+
348
+ let existingParams = {};
349
+ if (product?.action_params) {
350
+ try {
351
+ existingParams = JSON.parse(product.action_params);
352
+ } catch (e) {}
353
+ }
354
+
355
+ const actionOptions = [
356
+ `<option value="">no action</option>`,
357
+ ...allActions.map(a => `<option value="${a.key}" ${product && product.action === a.key ? 'selected' : ''}>${a.label}</option>`)
358
+ ].join('');
359
+
360
+ const selectedAction = product?.action || '';
361
+ const initialParams = selectedAction
362
+ ? buildActionParamFields(selectedAction, existingParams)
363
+ : '<div class="action-params-none">select an action first</div>';
364
+
365
+ const body = `
366
+ <div class="form-group">
367
+ <label>name</label>
368
+ <input type="text" id="productName" value="${product ? product.name : ''}" placeholder="enter product name">
369
+ </div>
370
+ <div class="form-group">
371
+ <label>description</label>
372
+ <textarea id="productDescription" placeholder="enter product description">${product ? product.description || '' : ''}</textarea>
373
+ </div>
374
+ <div class="form-group">
375
+ <label>price</label>
376
+ <input type="number" step="0.01" id="productPrice" value="${product ? product.price : ''}" placeholder="0.00">
377
+ </div>
378
+ <div class="form-group">
379
+ <label>category</label>
380
+ <select id="productCategory">
381
+ <option value="">no category</option>
382
+ ${allCategories.map(cat => `<option value="${cat.id}" ${product && product.category_id === cat.id ? 'selected' : ''}>${cat.name}</option>`).join('')}
383
+ </select>
384
+ </div>
385
+ <div class="form-group">
386
+ <label>image url</label>
387
+ <input type="text" id="productImageUrl" value="${product ? product.image_url || '' : ''}" placeholder="https://example.com/image.jpg">
388
+ </div>
389
+ <div class="form-group">
390
+ <label>stock</label>
391
+ <input type="number" id="productStock" value="${product ? product.stock : 0}" placeholder="0">
392
+ </div>
393
+ <div class="form-group">
394
+ <label>active</label>
395
+ <select id="productActive">
396
+ <option value="1" ${!product || product.active === 1 ? 'selected' : ''}>yes</option>
397
+ <option value="0" ${product && product.active === 0 ? 'selected' : ''}>no</option>
398
+ </select>
399
+ </div>
400
+ <div class="form-group">
401
+ <label>action</label>
402
+ <select id="productAction" onchange="onActionChange(this)">
403
+ ${actionOptions}
404
+ </select>
405
+ </div>
406
+ <div class="form-group">
407
+ <label>action params</label>
408
+ <div class="action-params-wrapper" id="actionParamsWrapper">
409
+ ${initialParams}
410
+ </div>
411
+ </div>
412
+ `;
413
+
414
+ currentForm = {type: 'product', data: product};
415
+ document.getElementById('formModalTitle').textContent = product ? 'edit product' : 'add product';
416
+ document.getElementById('formModalBody').innerHTML = body;
417
+ document.getElementById('formModal').classList.add('show');
418
+ }
419
+
420
+ function openCategoryForm(category = null) {
421
+ closeAdminPanel();
422
+ const body = `
423
+ <div class="form-group">
424
+ <label>name</label>
425
+ <input type="text" id="categoryName" value="${category ? category.name : ''}" placeholder="enter category name">
426
+ </div>
427
+ <div class="form-group">
428
+ <label>description</label>
429
+ <textarea id="categoryDescription" placeholder="enter category description">${category ? category.description || '' : ''}</textarea>
430
+ </div>
431
+ `;
432
+ currentForm = {type: 'category', data: category};
433
+ document.getElementById('formModalTitle').textContent = category ? 'edit category' : 'add category';
434
+ document.getElementById('formModalBody').innerHTML = body;
435
+ document.getElementById('formModal').classList.add('show');
436
+ }
437
+
438
+ function closeFormModal() {
439
+ document.getElementById('formModal').classList.remove('show');
440
+ currentForm = null;
441
+ openAdminPanel();
442
+ }
443
+
444
+ async function submitForm() {
445
+ if (!currentForm) return;
446
+ currentForm.type === 'product' ? await saveProduct() : await saveCategory();
447
+ }
448
+
449
+ async function saveProduct() {
450
+ const action = document.getElementById('productAction').value || null;
451
+
452
+ let action_params = null;
453
+ if (action) {
454
+ const actionDef = allActions.find(a => a.key === action);
455
+ if (actionDef && actionDef.params && actionDef.params.length > 0) {
456
+ const params = {};
457
+ actionDef.params.forEach(param => {
458
+ const el = document.getElementById(`actionParam_${param.key}`);
459
+ if (el) params[param.key] = param.type === 'number' ? parseFloat(el.value) : el.value;
460
+ });
461
+ action_params = params;
462
+ }
463
+ }
464
+
465
+ const data = {
466
+ name: document.getElementById('productName').value,
467
+ description: document.getElementById('productDescription').value,
468
+ price: parseFloat(document.getElementById('productPrice').value),
469
+ category_id: document.getElementById('productCategory').value || null,
470
+ image_url: document.getElementById('productImageUrl').value,
471
+ stock: parseInt(document.getElementById('productStock').value),
472
+ active: parseInt(document.getElementById('productActive').value),
473
+ action,
474
+ action_params
475
+ };
476
+
477
+ const isEdit = currentForm.data && currentForm.data.id;
478
+ const url = isEdit
479
+ ? `${config.basePath}${config.apiEndpoints.productUpdate}${currentForm.data.id}`
480
+ : `${config.basePath}${config.apiEndpoints.productCreate}`;
481
+
482
+ try {
483
+ const response = await fetch(url, {
484
+ method: 'POST',
485
+ headers: authHeaders(),
486
+ body: JSON.stringify(data)
487
+ });
488
+ const result = await response.json();
489
+ if (result.error) {
490
+ alert('error: ' + result.error);
491
+ } else {
492
+ document.getElementById('formModal').classList.remove('show');
493
+ currentForm = null;
494
+ await init();
495
+ openAdminPanel();
496
+ }
497
+ } catch (e) {
498
+ alert('error: ' + e.message);
499
+ }
500
+ }
501
+
502
+ async function saveCategory() {
503
+ const data = {
504
+ name: document.getElementById('categoryName').value,
505
+ description: document.getElementById('categoryDescription').value
506
+ };
507
+ const isEdit = currentForm.data && currentForm.data.id;
508
+ const url = isEdit
509
+ ? `${config.basePath}${config.apiEndpoints.categoryUpdate}${currentForm.data.id}`
510
+ : `${config.basePath}${config.apiEndpoints.categoryCreate}`;
511
+ try {
512
+ const response = await fetch(url, {
513
+ method: 'POST',
514
+ headers: authHeaders(),
515
+ body: JSON.stringify(data)
516
+ });
517
+ const result = await response.json();
518
+ if (result.error) {
519
+ alert('error: ' + result.error);
520
+ } else {
521
+ document.getElementById('formModal').classList.remove('show');
522
+ currentForm = null;
523
+ await init();
524
+ openAdminPanel();
525
+ }
526
+ } catch (e) {
527
+ alert('error: ' + e.message);
528
+ }
529
+ }
530
+
531
+ async function editProduct(id) {
532
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.product}${id}`, {
533
+ headers: authHeaders()
534
+ });
535
+ const data = await response.json();
536
+ data.error ? alert('error: ' + data.error) : openProductForm(data.product);
537
+ }
538
+
539
+ async function editCategory(id) {
540
+ const category = allCategories.find(c => c.id === id);
541
+ if (category) openCategoryForm(category);
542
+ }
543
+
544
+ async function deleteProduct(id) {
545
+ if (!confirm('delete this product?')) return;
546
+ try {
547
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.productDelete}${id}`, {
548
+ method: 'DELETE',
549
+ headers: authHeaders()
550
+ });
551
+ const result = await response.json();
552
+ if (result.error) {
553
+ alert('error: ' + result.error);
554
+ } else {
555
+ await loadAdminProducts();
556
+ await init();
557
+ }
558
+ } catch (e) {
559
+ alert('error: ' + e.message);
560
+ }
561
+ }
562
+
563
+ async function deleteCategory(id) {
564
+ if (!confirm('delete this category?')) return;
565
+ try {
566
+ const response = await fetch(`${config.basePath}${config.apiEndpoints.categoryDelete}${id}`, {
567
+ method: 'DELETE',
568
+ headers: authHeaders()
569
+ });
570
+ const result = await response.json();
571
+ if (result.error) {
572
+ alert('error: ' + result.error);
573
+ } else {
574
+ await loadAdminCategories();
575
+ await init();
576
+ }
577
+ } catch (e) {
578
+ alert('error: ' + e.message);
579
+ }
580
+ }
581
+
582
+ async function init() {
583
+ allCategories = await fetchCategories();
584
+ allActions = await fetchActions();
585
+ displayCategories(allCategories);
586
+ allProducts = await fetchProducts();
587
+ displayProducts(allProducts);
588
+ await checkAdminStatus();
589
+ }
590
+
591
+ document.getElementById('paymentModal').addEventListener('click', (e) => {
592
+ if (e.target.id === 'paymentModal') closeModal();
593
+ });
594
+
595
+ document.addEventListener('DOMContentLoaded', () => {
596
+ init();
597
+
598
+ const searchInput = document.getElementById('adminProductSearch');
599
+ if (searchInput) {
600
+ searchInput.addEventListener('input', (e) => {
601
+ adminProductSearch = e.target.value.trim();
602
+ renderAdminProducts(filterAdminProducts(allProducts));
603
+ });
604
+ }
605
+ });