NitinBot001 commited on
Commit
1f2ebfc
·
verified ·
1 Parent(s): e3f5d52

Create templates/index.html (#1)

Browse files

- Create templates/index.html (6333fb85f79930ff649a4818d9ea41080345218e)

Files changed (1) hide show
  1. templates/index.html +770 -0
templates/index.html ADDED
@@ -0,0 +1,770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Phone Specifications Search</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ color: #333;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ padding: 20px;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ margin-bottom: 40px;
30
+ color: white;
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 3rem;
35
+ margin-bottom: 10px;
36
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
37
+ }
38
+
39
+ .header p {
40
+ font-size: 1.2rem;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .search-container {
45
+ background: white;
46
+ border-radius: 20px;
47
+ padding: 30px;
48
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
49
+ margin-bottom: 30px;
50
+ }
51
+
52
+ .search-form {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 20px;
56
+ }
57
+
58
+ .input-group {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 8px;
62
+ }
63
+
64
+ .input-group label {
65
+ font-weight: 600;
66
+ color: #555;
67
+ }
68
+
69
+ .search-input {
70
+ padding: 15px;
71
+ border: 2px solid #e1e5e9;
72
+ border-radius: 10px;
73
+ font-size: 16px;
74
+ transition: all 0.3s ease;
75
+ }
76
+
77
+ .search-input:focus {
78
+ outline: none;
79
+ border-color: #667eea;
80
+ box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
81
+ }
82
+
83
+ .source-selector {
84
+ display: flex;
85
+ gap: 20px;
86
+ flex-wrap: wrap;
87
+ }
88
+
89
+ .source-option {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 8px;
93
+ }
94
+
95
+ .source-option input[type="radio"] {
96
+ accent-color: #667eea;
97
+ }
98
+
99
+ .button-group {
100
+ display: flex;
101
+ gap: 15px;
102
+ flex-wrap: wrap;
103
+ }
104
+
105
+ .btn {
106
+ padding: 15px 30px;
107
+ border: none;
108
+ border-radius: 10px;
109
+ font-size: 16px;
110
+ font-weight: 600;
111
+ cursor: pointer;
112
+ transition: all 0.3s ease;
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 8px;
116
+ }
117
+
118
+ .btn-primary {
119
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
+ color: white;
121
+ }
122
+
123
+ .btn-primary:hover {
124
+ transform: translateY(-2px);
125
+ box-shadow: 0 10px 20px rgba(102,126,234,0.3);
126
+ }
127
+
128
+ .btn-secondary {
129
+ background: #f8f9fa;
130
+ color: #495057;
131
+ border: 2px solid #e9ecef;
132
+ }
133
+
134
+ .btn-secondary:hover {
135
+ background: #e9ecef;
136
+ }
137
+
138
+ .btn:disabled {
139
+ opacity: 0.6;
140
+ cursor: not-allowed;
141
+ transform: none !important;
142
+ }
143
+
144
+ .loading {
145
+ display: none;
146
+ text-align: center;
147
+ padding: 20px;
148
+ }
149
+
150
+ .spinner {
151
+ border: 4px solid #f3f3f3;
152
+ border-top: 4px solid #667eea;
153
+ border-radius: 50%;
154
+ width: 40px;
155
+ height: 40px;
156
+ animation: spin 1s linear infinite;
157
+ margin: 0 auto 10px;
158
+ }
159
+
160
+ @keyframes spin {
161
+ 0% { transform: rotate(0deg); }
162
+ 100% { transform: rotate(360deg); }
163
+ }
164
+
165
+ .results-container {
166
+ display: none;
167
+ }
168
+
169
+ .phone-card {
170
+ background: white;
171
+ border-radius: 15px;
172
+ padding: 25px;
173
+ margin-bottom: 20px;
174
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
175
+ transition: transform 0.3s ease;
176
+ }
177
+
178
+ .phone-card:hover {
179
+ transform: translateY(-5px);
180
+ }
181
+
182
+ .phone-header {
183
+ display: flex;
184
+ justify-content: space-between;
185
+ align-items: center;
186
+ margin-bottom: 20px;
187
+ flex-wrap: wrap;
188
+ gap: 15px;
189
+ }
190
+
191
+ .phone-title {
192
+ font-size: 1.8rem;
193
+ font-weight: 700;
194
+ color: #333;
195
+ }
196
+
197
+ .phone-brand {
198
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
199
+ color: white;
200
+ padding: 5px 15px;
201
+ border-radius: 20px;
202
+ font-size: 0.9rem;
203
+ font-weight: 600;
204
+ }
205
+
206
+ .phone-images {
207
+ display: flex;
208
+ gap: 15px;
209
+ margin-bottom: 20px;
210
+ overflow-x: auto;
211
+ padding-bottom: 10px;
212
+ }
213
+
214
+ .phone-image {
215
+ width: 150px;
216
+ height: 200px;
217
+ object-fit: cover;
218
+ border-radius: 10px;
219
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
220
+ flex-shrink: 0;
221
+ }
222
+
223
+ .specs-container {
224
+ display: grid;
225
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
226
+ gap: 20px;
227
+ }
228
+
229
+ .spec-category {
230
+ background: #f8f9fa;
231
+ border-radius: 10px;
232
+ padding: 20px;
233
+ }
234
+
235
+ .spec-category h3 {
236
+ color: #495057;
237
+ margin-bottom: 15px;
238
+ font-size: 1.2rem;
239
+ border-bottom: 2px solid #dee2e6;
240
+ padding-bottom: 5px;
241
+ }
242
+
243
+ .spec-item {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ padding: 8px 0;
247
+ border-bottom: 1px solid #e9ecef;
248
+ }
249
+
250
+ .spec-item:last-child {
251
+ border-bottom: none;
252
+ }
253
+
254
+ .spec-label {
255
+ font-weight: 600;
256
+ color: #495057;
257
+ }
258
+
259
+ .spec-value {
260
+ color: #6c757d;
261
+ text-align: right;
262
+ max-width: 60%;
263
+ }
264
+
265
+ .multiple-phones-section {
266
+ background: white;
267
+ border-radius: 20px;
268
+ padding: 30px;
269
+ margin-top: 30px;
270
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
271
+ }
272
+
273
+ .multiple-phones-section h2 {
274
+ margin-bottom: 20px;
275
+ color: #333;
276
+ }
277
+
278
+ .phone-list-input {
279
+ width: 100%;
280
+ min-height: 100px;
281
+ padding: 15px;
282
+ border: 2px solid #e1e5e9;
283
+ border-radius: 10px;
284
+ font-size: 16px;
285
+ resize: vertical;
286
+ margin-bottom: 20px;
287
+ }
288
+
289
+ .status-indicator {
290
+ display: inline-block;
291
+ width: 10px;
292
+ height: 10px;
293
+ border-radius: 50%;
294
+ margin-right: 8px;
295
+ }
296
+
297
+ .status-healthy { background-color: #28a745; }
298
+ .status-error { background-color: #dc3545; }
299
+
300
+ .alert {
301
+ padding: 15px;
302
+ border-radius: 10px;
303
+ margin-bottom: 20px;
304
+ }
305
+
306
+ .alert-success {
307
+ background-color: #d4edda;
308
+ border: 1px solid #c3e6cb;
309
+ color: #155724;
310
+ }
311
+
312
+ .alert-error {
313
+ background-color: #f8d7da;
314
+ border: 1px solid #f5c6cb;
315
+ color: #721c24;
316
+ }
317
+
318
+ .export-section {
319
+ margin-top: 20px;
320
+ padding-top: 20px;
321
+ border-top: 2px solid #e9ecef;
322
+ }
323
+
324
+ @media (max-width: 768px) {
325
+ .container {
326
+ padding: 10px;
327
+ }
328
+
329
+ .header h1 {
330
+ font-size: 2rem;
331
+ }
332
+
333
+ .search-container,
334
+ .multiple-phones-section {
335
+ padding: 20px;
336
+ }
337
+
338
+ .button-group {
339
+ flex-direction: column;
340
+ }
341
+
342
+ .btn {
343
+ width: 100%;
344
+ justify-content: center;
345
+ }
346
+
347
+ .phone-header {
348
+ flex-direction: column;
349
+ align-items: flex-start;
350
+ }
351
+
352
+ .specs-container {
353
+ grid-template-columns: 1fr;
354
+ }
355
+ }
356
+ </style>
357
+ </head>
358
+ <body>
359
+ <div class="container">
360
+ <div class="header">
361
+ <h1>📱 Phone Specifications Search</h1>
362
+ <p>Search and compare phone specifications from multiple sources</p>
363
+ </div>
364
+
365
+ <!-- API Status -->
366
+ <div id="api-status" class="alert alert-success">
367
+ <span class="status-indicator status-healthy"></span>
368
+ API Status: Checking...
369
+ </div>
370
+
371
+ <!-- Single Phone Search -->
372
+ <div class="search-container">
373
+ <h2>Search Single Phone</h2>
374
+ <form class="search-form" id="single-search-form">
375
+ <div class="input-group">
376
+ <label for="phone-name">Phone Name</label>
377
+ <input
378
+ type="text"
379
+ id="phone-name"
380
+ class="search-input"
381
+ placeholder="e.g., iPhone 15 Pro, Samsung Galaxy S24, OnePlus 12"
382
+ required
383
+ >
384
+ </div>
385
+
386
+ <div class="input-group">
387
+ <label>Data Source</label>
388
+ <div class="source-selector">
389
+ <div class="source-option">
390
+ <input type="radio" id="gsmarena" name="source" value="gsmarena" checked>
391
+ <label for="gsmarena">GSMArena</label>
392
+ </div>
393
+ <div class="source-option">
394
+ <input type="radio" id="phonedb" name="source" value="phonedb">
395
+ <label for="phonedb">PhoneDB</label>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <div class="button-group">
401
+ <button type="submit" class="btn btn-primary">
402
+ 🔍 Search Phone
403
+ </button>
404
+ <button type="button" class="btn btn-secondary" onclick="clearResults()">
405
+ 🗑️ Clear Results
406
+ </button>
407
+ </div>
408
+ </form>
409
+ </div>
410
+
411
+ <!-- Loading indicator -->
412
+ <div id="loading" class="loading">
413
+ <div class="spinner"></div>
414
+ <p>Searching for phone specifications...</p>
415
+ </div>
416
+
417
+ <!-- Results container -->
418
+ <div id="results" class="results-container"></div>
419
+
420
+ <!-- Multiple Phones Search -->
421
+ <div class="multiple-phones-section">
422
+ <h2>Search Multiple Phones</h2>
423
+ <p style="margin-bottom: 20px; color: #6c757d;">Enter phone names separated by new lines (one phone per line)</p>
424
+
425
+ <textarea
426
+ id="phone-list"
427
+ class="phone-list-input"
428
+ placeholder="iPhone 15 Pro&#10;Samsung Galaxy S24&#10;OnePlus 12&#10;Google Pixel 8"
429
+ ></textarea>
430
+
431
+ <div class="input-group">
432
+ <label>Data Source</label>
433
+ <div class="source-selector">
434
+ <div class="source-option">
435
+ <input type="radio" id="gsmarena-multi" name="source-multi" value="gsmarena" checked>
436
+ <label for="gsmarena-multi">GSMArena</label>
437
+ </div>
438
+ <div class="source-option">
439
+ <input type="radio" id="phonedb-multi" name="source-multi" value="phonedb">
440
+ <label for="phonedb-multi">PhoneDB</label>
441
+ </div>
442
+ </div>
443
+ </div>
444
+
445
+ <div class="button-group">
446
+ <button class="btn btn-primary" onclick="searchMultiplePhones()">
447
+ 🔍 Search All Phones
448
+ </button>
449
+ <button class="btn btn-secondary" onclick="startBackgroundSearch()">
450
+ ⏱️ Background Search
451
+ </button>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <script>
457
+ const API_BASE = window.location.origin;
458
+
459
+ // Check API health on load
460
+ window.addEventListener('load', checkApiHealth);
461
+
462
+ async function checkApiHealth() {
463
+ try {
464
+ const response = await fetch(`${API_BASE}/health`);
465
+ const data = await response.json();
466
+
467
+ const statusEl = document.getElementById('api-status');
468
+ if (data.success) {
469
+ statusEl.className = 'alert alert-success';
470
+ statusEl.innerHTML = `
471
+ <span class="status-indicator status-healthy"></span>
472
+ API Status: Healthy (GSMArena: ${data.data.scrapers.gsmarena ? '✅' : '❌'}, PhoneDB: ${data.data.scrapers.phonedb ? '✅' : '❌'})
473
+ `;
474
+ } else {
475
+ throw new Error('API not healthy');
476
+ }
477
+ } catch (error) {
478
+ const statusEl = document.getElementById('api-status');
479
+ statusEl.className = 'alert alert-error';
480
+ statusEl.innerHTML = `
481
+ <span class="status-indicator status-error"></span>
482
+ API Status: Error - ${error.message}
483
+ `;
484
+ }
485
+ }
486
+
487
+ // Single phone search
488
+ document.getElementById('single-search-form').addEventListener('submit', async (e) => {
489
+ e.preventDefault();
490
+
491
+ const phoneName = document.getElementById('phone-name').value.trim();
492
+ const source = document.querySelector('input[name="source"]:checked').value;
493
+
494
+ if (!phoneName) {
495
+ alert('Please enter a phone name');
496
+ return;
497
+ }
498
+
499
+ showLoading(true);
500
+ clearResults();
501
+
502
+ try {
503
+ const response = await fetch(`${API_BASE}/api/search`, {
504
+ method: 'POST',
505
+ headers: {
506
+ 'Content-Type': 'application/json',
507
+ },
508
+ body: JSON.stringify({
509
+ phone_name: phoneName,
510
+ source: source
511
+ })
512
+ });
513
+
514
+ const data = await response.json();
515
+
516
+ if (data.success && data.data) {
517
+ displayPhoneResults([data.data]);
518
+ } else {
519
+ showError(data.message || 'No results found');
520
+ }
521
+ } catch (error) {
522
+ showError('Error searching for phone: ' + error.message);
523
+ } finally {
524
+ showLoading(false);
525
+ }
526
+ });
527
+
528
+ // Multiple phones search
529
+ async function searchMultiplePhones() {
530
+ const phoneList = document.getElementById('phone-list').value.trim();
531
+ const source = document.querySelector('input[name="source-multi"]:checked').value;
532
+
533
+ if (!phoneList) {
534
+ alert('Please enter phone names');
535
+ return;
536
+ }
537
+
538
+ const phoneNames = phoneList.split('\n')
539
+ .map(name => name.trim())
540
+ .filter(name => name.length > 0);
541
+
542
+ if (phoneNames.length === 0) {
543
+ alert('Please enter valid phone names');
544
+ return;
545
+ }
546
+
547
+ showLoading(true);
548
+ clearResults();
549
+
550
+ try {
551
+ const response = await fetch(`${API_BASE}/api/search/multiple`, {
552
+ method: 'POST',
553
+ headers: {
554
+ 'Content-Type': 'application/json',
555
+ },
556
+ body: JSON.stringify({
557
+ phone_names: phoneNames,
558
+ source: source
559
+ })
560
+ });
561
+
562
+ const data = await response.json();
563
+
564
+ if (data.success && data.data.phones.length > 0) {
565
+ displayPhoneResults(data.data.phones);
566
+ showSuccess(`Successfully found ${data.data.success_count}/${data.data.total_count} phones`);
567
+ } else {
568
+ showError(data.message || 'No results found');
569
+ }
570
+ } catch (error) {
571
+ showError('Error searching for phones: ' + error.message);
572
+ } finally {
573
+ showLoading(false);
574
+ }
575
+ }
576
+
577
+ // Background search
578
+ async function startBackgroundSearch() {
579
+ const phoneList = document.getElementById('phone-list').value.trim();
580
+ const source = document.querySelector('input[name="source-multi"]:checked').value;
581
+
582
+ if (!phoneList) {
583
+ alert('Please enter phone names');
584
+ return;
585
+ }
586
+
587
+ const phoneNames = phoneList.split('\n')
588
+ .map(name => name.trim())
589
+ .filter(name => name.length > 0);
590
+
591
+ try {
592
+ const response = await fetch(`${API_BASE}/api/scrape/background`, {
593
+ method: 'POST',
594
+ headers: {
595
+ 'Content-Type': 'application/json',
596
+ },
597
+ body: JSON.stringify({
598
+ phone_names: phoneNames,
599
+ source: source
600
+ })
601
+ });
602
+
603
+ const data = await response.json();
604
+
605
+ if (data.success) {
606
+ const jobId = data.data.job_id;
607
+ showSuccess(`Background job started: ${jobId}`);
608
+ monitorBackgroundJob(jobId);
609
+ } else {
610
+ showError('Failed to start background job');
611
+ }
612
+ } catch (error) {
613
+ showError('Error starting background job: ' + error.message);
614
+ }
615
+ }
616
+
617
+ // Monitor background job
618
+ async function monitorBackgroundJob(jobId) {
619
+ const checkStatus = async () => {
620
+ try {
621
+ const response = await fetch(`${API_BASE}/api/scrape/status/${jobId}`);
622
+ const data = await response.json();
623
+
624
+ if (data.success) {
625
+ const job = data.data;
626
+ const progress = `${job.progress}/${job.total}`;
627
+
628
+ if (job.status === 'completed') {
629
+ displayPhoneResults(job.results);
630
+ showSuccess(`Background job completed: ${progress} phones processed`);
631
+ return;
632
+ } else if (job.status === 'failed') {
633
+ showError(`Background job failed: ${job.error || 'Unknown error'}`);
634
+ return;
635
+ } else {
636
+ showSuccess(`Background job running: ${progress} phones processed${job.current_phone ? ` (Current: ${job.current_phone})` : ''}`);
637
+ setTimeout(checkStatus, 3000); // Check every 3 seconds
638
+ }
639
+ }
640
+ } catch (error) {
641
+ showError('Error monitoring job: ' + error.message);
642
+ }
643
+ };
644
+
645
+ checkStatus();
646
+ }
647
+
648
+ // Display results
649
+ function displayPhoneResults(phones) {
650
+ const resultsContainer = document.getElementById('results');
651
+ resultsContainer.innerHTML = '';
652
+
653
+ phones.forEach(phone => {
654
+ const phoneCard = createPhoneCard(phone);
655
+ resultsContainer.appendChild(phoneCard);
656
+ });
657
+
658
+ resultsContainer.style.display = 'block';
659
+ }
660
+
661
+ // Create phone card
662
+ function createPhoneCard(phone) {
663
+ const card = document.createElement('div');
664
+ card.className = 'phone-card';
665
+
666
+ // Images HTML
667
+ const imagesHtml = phone.images && phone.images.length > 0
668
+ ? `<div class="phone-images">
669
+ ${phone.images.map(img => `<img src="${img}" alt="${phone.name}" class="phone-image" onerror="this.style.display='none'">`).join('')}
670
+ </div>`
671
+ : '';
672
+
673
+ // Specifications HTML
674
+ const specsHtml = Object.entries(phone.specifications || {})
675
+ .map(([category, specs]) => {
676
+ if (typeof specs === 'object' && specs !== null) {
677
+ const specItems = Object.entries(specs)
678
+ .map(([key, value]) => `
679
+ <div class="spec-item">
680
+ <span class="spec-label">${key}</span>
681
+ <span class="spec-value">${value}</span>
682
+ </div>
683
+ `).join('');
684
+
685
+ return `
686
+ <div class="spec-category">
687
+ <h3>${category}</h3>
688
+ ${specItems}
689
+ </div>
690
+ `;
691
+ }
692
+ return '';
693
+ }).join('');
694
+
695
+ card.innerHTML = `
696
+ <div class="phone-header">
697
+ <h2 class="phone-title">${phone.name}</h2>
698
+ <span class="phone-brand">${phone.brand}</span>
699
+ </div>
700
+
701
+ ${imagesHtml}
702
+
703
+ <div class="specs-container">
704
+ ${specsHtml}
705
+ </div>
706
+
707
+ <div class="export-section">
708
+ <button class="btn btn-secondary" onclick="exportPhoneData('${phone.name}')">
709
+ 💾 Export JSON
710
+ </button>
711
+ <a href="${phone.source_url}" target="_blank" class="btn btn-secondary">
712
+ 🔗 View Source
713
+ </a>
714
+ </div>
715
+ `;
716
+
717
+ return card;
718
+ }
719
+
720
+ // Export phone data
721
+ async function exportPhoneData(phoneName) {
722
+ try {
723
+ const source = document.querySelector('input[name="source"]:checked').value;
724
+ window.open(`${API_BASE}/api/export/${encodeURIComponent(phoneName)}?source=${source}`, '_blank');
725
+ } catch (error) {
726
+ showError('Error exporting data: ' + error.message);
727
+ }
728
+ }
729
+
730
+ // Utility functions
731
+ function showLoading(show) {
732
+ document.getElementById('loading').style.display = show ? 'block' : 'none';
733
+ }
734
+
735
+ function clearResults() {
736
+ document.getElementById('results').style.display = 'none';
737
+ document.getElementById('results').innerHTML = '';
738
+ }
739
+
740
+ function showSuccess(message) {
741
+ showAlert(message, 'success');
742
+ }
743
+
744
+ function showError(message) {
745
+ showAlert(message, 'error');
746
+ }
747
+
748
+ function showAlert(message, type) {
749
+ // Remove existing alerts
750
+ const existingAlerts = document.querySelectorAll('.alert:not(#api-status)');
751
+ existingAlerts.forEach(alert => alert.remove());
752
+
753
+ const alert = document.createElement('div');
754
+ alert.className = `alert alert-${type}`;
755
+ alert.textContent = message;
756
+
757
+ const container = document.querySelector('.container');
758
+ const apiStatus = document.getElementById('api-status');
759
+ container.insertBefore(alert, apiStatus.nextSibling);
760
+
761
+ // Auto remove after 5 seconds
762
+ setTimeout(() => {
763
+ if (alert.parentNode) {
764
+ alert.remove();
765
+ }
766
+ }, 5000);
767
+ }
768
+ </script>
769
+ </body>
770
+ </html>