NitinBot001 commited on
Commit
561972a
·
verified ·
1 Parent(s): a6c126b

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +444 -541
templates/index.html CHANGED
@@ -10,347 +10,325 @@
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>
@@ -358,146 +336,70 @@
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`, {
@@ -506,46 +408,47 @@
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`, {
@@ -558,14 +461,15 @@
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);
@@ -573,198 +477,197 @@
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>
 
10
  padding: 0;
11
  box-sizing: border-box;
12
  }
13
+
14
  body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
  min-height: 100vh;
18
+ padding: 20px;
19
  }
20
+
21
  .container {
22
  max-width: 1200px;
23
  margin: 0 auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
  }
29
+
30
  .header {
31
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
 
32
  color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
  }
36
+
37
  .header h1 {
38
+ font-size: 2.5rem;
39
  margin-bottom: 10px;
40
+ font-weight: 700;
41
  }
42
+
43
  .header p {
44
+ font-size: 1.1rem;
45
  opacity: 0.9;
46
  }
47
+
48
+ .search-section {
49
+ padding: 40px;
50
+ background: #f8f9fa;
 
 
 
51
  }
52
+
53
  .search-form {
54
  display: flex;
55
+ gap: 15px;
56
+ margin-bottom: 20px;
57
+ flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
 
58
  }
59
+
60
  .search-input {
61
+ flex: 1;
62
+ min-width: 250px;
63
+ padding: 15px 20px;
64
  border: 2px solid #e1e5e9;
65
  border-radius: 10px;
66
  font-size: 16px;
67
  transition: all 0.3s ease;
68
  }
69
+
70
  .search-input:focus {
71
  outline: none;
72
  border-color: #667eea;
73
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
74
  }
75
+
76
+ .source-select {
77
+ padding: 15px 20px;
78
+ border: 2px solid #e1e5e9;
79
+ border-radius: 10px;
80
+ font-size: 16px;
81
+ background: white;
82
+ min-width: 150px;
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
+
85
+ .search-btn, .multiple-btn {
86
  padding: 15px 30px;
87
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
88
+ color: white;
89
  border: none;
90
  border-radius: 10px;
91
  font-size: 16px;
92
  font-weight: 600;
93
  cursor: pointer;
94
  transition: all 0.3s ease;
95
+ white-space: nowrap;
 
 
96
  }
97
+
98
+ .search-btn:hover, .multiple-btn:hover {
 
 
 
 
 
99
  transform: translateY(-2px);
100
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
 
 
 
 
 
 
 
 
 
 
101
  }
102
+
103
+ .search-btn:disabled, .multiple-btn:disabled {
104
  opacity: 0.6;
105
  cursor: not-allowed;
106
+ transform: none;
107
+ }
108
+
109
+ .multiple-search {
110
+ margin-top: 20px;
111
+ padding-top: 20px;
112
+ border-top: 1px solid #e1e5e9;
113
+ }
114
+
115
+ .multiple-input {
116
+ width: 100%;
117
+ padding: 15px 20px;
118
+ border: 2px solid #e1e5e9;
119
+ border-radius: 10px;
120
+ font-size: 16px;
121
+ resize: vertical;
122
+ min-height: 100px;
123
+ margin-bottom: 15px;
124
  }
125
+
126
  .loading {
127
  display: none;
128
  text-align: center;
129
+ padding: 40px;
130
  }
131
+
132
  .spinner {
133
  border: 4px solid #f3f3f3;
134
  border-top: 4px solid #667eea;
135
  border-radius: 50%;
136
+ width: 50px;
137
+ height: 50px;
138
  animation: spin 1s linear infinite;
139
+ margin: 0 auto 20px;
140
  }
141
+
142
  @keyframes spin {
143
  0% { transform: rotate(0deg); }
144
  100% { transform: rotate(360deg); }
145
  }
146
+
147
+ .results-section {
148
+ padding: 40px;
149
  display: none;
150
  }
151
+
152
  .phone-card {
153
  background: white;
154
  border-radius: 15px;
 
 
155
  box-shadow: 0 10px 30px rgba(0,0,0,0.1);
156
+ margin-bottom: 30px;
157
+ overflow: hidden;
158
+ border: 1px solid #e1e5e9;
 
 
159
  }
160
+
161
  .phone-header {
162
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
163
+ color: white;
164
+ padding: 25px;
165
  display: flex;
166
  justify-content: space-between;
167
  align-items: center;
 
168
  flex-wrap: wrap;
 
169
  }
170
+
171
  .phone-title {
172
  font-size: 1.8rem;
173
  font-weight: 700;
 
174
  }
175
+
176
  .phone-brand {
177
+ font-size: 1rem;
178
+ opacity: 0.9;
179
+ margin-top: 5px;
180
+ }
181
+
182
+ .export-btn {
183
+ background: rgba(255,255,255,0.2);
184
  color: white;
185
+ border: 1px solid rgba(255,255,255,0.3);
186
+ padding: 10px 20px;
187
+ border-radius: 8px;
188
+ cursor: pointer;
189
+ font-size: 14px;
190
+ transition: all 0.3s ease;
191
  }
192
+
193
+ .export-btn:hover {
194
+ background: rgba(255,255,255,0.3);
195
+ }
196
+
197
+ .phone-content {
198
  display: flex;
199
+ gap: 30px;
200
+ padding: 30px;
201
+ flex-wrap: wrap;
202
+ }
203
+
204
+ .phone-images {
205
+ flex: 0 0 300px;
206
+ min-width: 250px;
207
  }
208
+
209
  .phone-image {
210
+ width: 100%;
211
+ height: 400px;
212
+ object-fit: contain;
213
  border-radius: 10px;
214
+ background: #f8f9fa;
215
+ border: 1px solid #e1e5e9;
216
+ margin-bottom: 10px;
217
  }
218
+
219
+ .image-nav {
220
+ display: flex;
221
+ gap: 5px;
222
+ justify-content: center;
223
  }
224
+
225
+ .image-dot {
226
+ width: 10px;
227
+ height: 10px;
228
+ border-radius: 50%;
229
+ background: #ddd;
230
+ cursor: pointer;
231
+ transition: all 0.3s ease;
232
  }
233
+
234
+ .image-dot.active {
235
+ background: #667eea;
 
 
 
 
236
  }
237
+
238
+ .specs-container {
239
+ flex: 1;
240
+ min-width: 300px;
 
 
241
  }
242
+
243
+ .specs-table {
244
+ width: 100%;
245
+ border-collapse: collapse;
246
+ font-size: 14px;
247
  }
248
+
249
+ .specs-table th {
250
+ background: #f8f9fa;
251
+ color: #2c3e50;
252
  font-weight: 600;
253
+ padding: 15px 20px;
254
+ text-align: left;
255
+ border-bottom: 2px solid #e1e5e9;
256
+ font-size: 16px;
257
  }
258
+
259
+ .specs-table td {
260
+ padding: 12px 20px;
261
+ border-bottom: 1px solid #e1e5e9;
262
+ vertical-align: top;
263
  }
264
+
265
+ .specs-table tr:hover {
266
+ background: #f8f9fa;
 
 
 
 
267
  }
268
+
269
+ .spec-category {
270
+ font-weight: 600;
271
+ color: #667eea;
272
+ background: #f0f2ff !important;
273
  }
274
+
275
+ .spec-key {
276
+ font-weight: 500;
277
+ color: #2c3e50;
278
+ width: 30%;
 
 
 
 
 
279
  }
280
+
281
+ .spec-value {
282
+ color: #555;
283
+ line-height: 1.5;
 
 
 
 
 
 
 
 
 
 
 
 
284
  }
285
+
286
+ .error-message {
287
+ background: #fee;
288
+ border: 1px solid #fcc;
289
+ color: #c33;
290
+ padding: 20px;
291
+ border-radius: 10px;
292
+ margin: 20px 0;
293
+ text-align: center;
294
  }
295
+
296
+ .success-message {
297
+ background: #efe;
298
+ border: 1px solid #cfc;
299
+ color: #363;
300
+ padding: 20px;
301
+ border-radius: 10px;
302
+ margin: 20px 0;
303
+ text-align: center;
304
  }
305
+
306
+ .multiple-results {
307
+ display: grid;
308
+ gap: 20px;
 
309
  }
310
+
311
  @media (max-width: 768px) {
312
+ .search-form {
313
+ flex-direction: column;
 
 
 
 
314
  }
315
+
316
+ .search-input, .source-select {
317
+ min-width: 100%;
 
318
  }
319
+
320
+ .phone-content {
321
  flex-direction: column;
322
  }
323
+
324
+ .phone-images {
325
+ flex: none;
 
326
  }
327
+
328
  .phone-header {
329
  flex-direction: column;
330
  align-items: flex-start;
331
+ gap: 15px;
 
 
 
332
  }
333
  }
334
  </style>
 
336
  <body>
337
  <div class="container">
338
  <div class="header">
339
+ <h1>📱 Phone Specifications</h1>
340
+ <p>Search and compare detailed phone specifications from multiple sources</p>
341
  </div>
342
 
343
+ <div class="search-section">
344
+ <div class="search-form">
345
+ <input type="text"
346
+ class="search-input"
347
+ id="phoneSearch"
348
+ placeholder="Enter phone name (e.g., iPhone 15 Pro, Samsung Galaxy S24)"
349
+ onkeypress="handleKeyPress(event)">
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
+ <select class="source-select" id="sourceSelect">
352
+ <option value="gsmarena">GSMArena</option>
353
+ <option value="phonedb">PhoneDB</option>
354
+ </select>
 
 
 
 
 
 
 
 
 
355
 
356
+ <button class="search-btn" onclick="searchPhone()" id="searchBtn">
357
+ 🔍 Search Phone
358
+ </button>
359
+ </div>
360
+
361
+ <div class="multiple-search">
362
+ <h3 style="margin-bottom: 15px; color: #2c3e50;">Search Multiple Phones</h3>
363
+ <textarea class="multiple-input"
364
+ id="multiplePhones"
365
+ placeholder="Enter phone names, one per line:&#10;iPhone 15 Pro&#10;Samsung Galaxy S24&#10;Google Pixel 8"></textarea>
366
+ <button class="multiple-btn" onclick="searchMultiplePhones()" id="multipleBt">
367
+ 🔍 Search Multiple Phones
368
+ </button>
369
+ </div>
370
  </div>
371
 
372
+ <div class="loading" id="loading">
 
373
  <div class="spinner"></div>
374
  <p>Searching for phone specifications...</p>
375
  </div>
376
 
377
+ <div class="results-section" id="results">
378
+ <!-- Results will be populated here -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  </div>
380
  </div>
381
 
382
  <script>
383
  const API_BASE = window.location.origin;
384
+ let currentResults = [];
385
+
386
+ function handleKeyPress(event) {
387
+ if (event.key === 'Enter') {
388
+ searchPhone();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
  }
391
+
392
+ async function searchPhone() {
393
+ const phoneSearching = document.getElementById('phoneSearch').value.trim();
394
+ const source = document.getElementById('sourceSelect').value;
 
 
 
395
 
396
+ if (!phoneSearching) {
397
+ showError('Please enter a phone name');
398
  return;
399
  }
400
+
401
  showLoading(true);
402
+ hideResults();
403
 
404
  try {
405
  const response = await fetch(`${API_BASE}/api/search`, {
 
408
  'Content-Type': 'application/json',
409
  },
410
  body: JSON.stringify({
411
+ phone_name: phoneSearching,
412
  source: source
413
  })
414
  });
415
+
416
+ const result = await response.json();
417
 
418
+ if (result.success && result.data) {
419
+ currentResults = [result.data];
420
+ displayResults([result.data]);
421
+ showSuccess(`Found specifications for ${result.data.name}`);
422
  } else {
423
+ showError(result.message || 'Phone not found');
424
  }
425
  } catch (error) {
426
  showError('Error searching for phone: ' + error.message);
427
  } finally {
428
  showLoading(false);
429
  }
430
+ }
431
+
 
432
  async function searchMultiplePhones() {
433
+ const phonesText = document.getElementById('multiplePhones').value.trim();
434
+ const source = document.getElementById('sourceSelect').value;
435
 
436
+ if (!phonesText) {
437
+ showError('Please enter phone names');
438
  return;
439
  }
440
+
441
+ const phoneNames = phonesText.split('\n')
442
  .map(name => name.trim())
443
  .filter(name => name.length > 0);
444
+
445
  if (phoneNames.length === 0) {
446
+ showError('Please enter valid phone names');
447
  return;
448
  }
449
+
450
  showLoading(true);
451
+ hideResults();
452
 
453
  try {
454
  const response = await fetch(`${API_BASE}/api/search/multiple`, {
 
461
  source: source
462
  })
463
  });
464
+
465
+ const result = await response.json();
466
 
467
+ if (result.success && result.data && result.data.phones) {
468
+ currentResults = result.data.phones;
469
+ displayResults(result.data.phones);
470
+ showSuccess(`Found ${result.data.success_count} out of ${result.data.total_count} phones`);
 
471
  } else {
472
+ showError(result.message || 'No phones found');
473
  }
474
  } catch (error) {
475
  showError('Error searching for phones: ' + error.message);
 
477
  showLoading(false);
478
  }
479
  }
480
+
481
+ function displayResults(phones) {
482
+ const resultsDiv = document.getElementById('results');
483
+ resultsDiv.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ phones.forEach((phone, index) => {
486
+ const phoneCard = createPhoneCard(phone, index);
487
+ resultsDiv.appendChild(phoneCard);
488
  });
489
 
490
+ showResults();
491
  }
492
+
493
+ function createPhoneCard(phone, index) {
 
494
  const card = document.createElement('div');
495
  card.className = 'phone-card';
496
 
497
+ const images = phone.images && phone.images.length > 0 ? phone.images : ['/api/placeholder/300/400'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  card.innerHTML = `
500
  <div class="phone-header">
501
+ <div>
502
+ <div class="phone-title">${phone.name || 'Unknown Phone'}</div>
503
+ <div class="phone-brand">${phone.brand || 'Unknown Brand'}</div>
504
+ </div>
505
+ <button class="export-btn" onclick="exportPhoneData('${phone.name}')">
506
+ 📥 Export JSON
 
 
 
 
 
 
 
507
  </button>
508
+ </div>
509
+ <div class="phone-content">
510
+ <div class="phone-images">
511
+ <img class="phone-image" id="phoneImage${index}" src="${images[0]}" alt="${phone.name}"
512
+ onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjQwMCIgdmlld0JveD0iMCAwIDMwMCA0MDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iNDAwIiBmaWxsPSIjRjhGOUZBIi8+CjxwYXRoIGQ9Ik0xMjUgMTc1SDE3NVYyMjVIMTI1VjE3NVoiIGZpbGw9IiNEREREREQiLz4KPHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkgMTJMMTEgMTRMMTUgMTBNMjEgMTJDMjEgMTYuOTcwNiAxNi45NzA2IDIxIDEyIDIxQzcuMDI5NDQgMjEgMyAxNi45NzA2IDMgMTJDMyA3LjAyOTQ0IDcuMDI5NDQgMyAxMiAzQzE2Ljk3MDYgMyAyMSA3LjAyOTQ0IDIxIDEyWiIgc3Ryb2tlPSIjREREREREIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K'">
513
+ ${images.length > 1 ? createImageNavigation(images, index) : ''}
514
+ </div>
515
+ <div class="specs-container">
516
+ ${createSpecsTable(phone.specifications)}
517
+ </div>
518
  </div>
519
  `;
520
 
521
  return card;
522
  }
523
+
524
+ function createImageNavigation(images, phoneIndex) {
525
+ const dots = images.map((_, imgIndex) =>
526
+ `<div class="image-dot ${imgIndex === 0 ? 'active' : ''}"
527
+ onclick="changeImage(${phoneIndex}, ${imgIndex})"></div>`
528
+ ).join('');
529
+
530
+ return `<div class="image-nav">${dots}</div>`;
531
+ }
532
+
533
+ function changeImage(phoneIndex, imageIndex) {
534
+ const phone = currentResults[phoneIndex];
535
+ const imgElement = document.getElementById(`phoneImage${phoneIndex}`);
536
+ imgElement.src = phone.images[imageIndex];
537
+
538
+ // Update active dot
539
+ const card = imgElement.closest('.phone-card');
540
+ const dots = card.querySelectorAll('.image-dot');
541
+ dots.forEach((dot, index) => {
542
+ dot.classList.toggle('active', index === imageIndex);
543
+ });
544
+ }
545
+
546
+ function createSpecsTable(specs) {
547
+ if (!specs || typeof specs !== 'object') {
548
+ return '<p>No specifications available</p>';
549
+ }
550
+
551
+ let tableHTML = '<table class="specs-table"><thead><tr><th colspan="2">Specifications</th></tr></thead><tbody>';
552
+
553
+ Object.entries(specs).forEach(([category, categorySpecs]) => {
554
+ if (categorySpecs && typeof categorySpecs === 'object') {
555
+ // Category header
556
+ tableHTML += `<tr class="spec-category"><td colspan="2">${formatCategoryName(category)}</td></tr>`;
557
+
558
+ // Category specifications
559
+ Object.entries(categorySpecs).forEach(([key, value]) => {
560
+ if (value) {
561
+ tableHTML += `
562
+ <tr>
563
+ <td class="spec-key">${formatSpecName(key)}</td>
564
+ <td class="spec-value">${formatSpecValue(value)}</td>
565
+ </tr>
566
+ `;
567
+ }
568
+ });
569
+ } else if (categorySpecs) {
570
+ // Direct specification
571
+ tableHTML += `
572
+ <tr>
573
+ <td class="spec-key">${formatSpecName(category)}</td>
574
+ <td class="spec-value">${formatSpecValue(categorySpecs)}</td>
575
+ </tr>
576
+ `;
577
+ }
578
+ });
579
+
580
+ tableHTML += '</tbody></table>';
581
+ return tableHTML;
582
+ }
583
+
584
+ function formatCategoryName(name) {
585
+ return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
586
+ }
587
+
588
+ function formatSpecName(name) {
589
+ return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
590
+ }
591
+
592
+ function formatSpecValue(value) {
593
+ if (Array.isArray(value)) {
594
+ return value.join(', ');
595
+ }
596
+ if (typeof value === 'object') {
597
+ return JSON.stringify(value, null, 2);
598
+ }
599
+ return value.toString();
600
+ }
601
+
602
  async function exportPhoneData(phoneName) {
603
+ const source = document.getElementById('sourceSelect').value;
604
  try {
605
+ const response = await fetch(`${API_BASE}/api/export/${encodeURIComponent(phoneName)}?source=${source}`);
606
+
607
+ if (response.ok) {
608
+ const blob = await response.blob();
609
+ const url = window.URL.createObjectURL(blob);
610
+ const a = document.createElement('a');
611
+ a.href = url;
612
+ a.download = `${phoneName.replace(/\s+/g, '_')}_specs.json`;
613
+ document.body.appendChild(a);
614
+ a.click();
615
+ document.body.removeChild(a);
616
+ window.URL.revokeObjectURL(url);
617
+ showSuccess('Phone data exported successfully!');
618
+ } else {
619
+ showError('Failed to export phone data');
620
+ }
621
  } catch (error) {
622
+ showError('Error exporting phone data: ' + error.message);
623
  }
624
  }
625
+
 
626
  function showLoading(show) {
627
+ const loading = document.getElementById('loading');
628
+ const searchBtn = document.getElementById('searchBtn');
629
+ const multipleBtn = document.getElementById('multipleBt');
630
+
631
+ loading.style.display = show ? 'block' : 'none';
632
+ searchBtn.disabled = show;
633
+ multipleBtn.disabled = show;
634
  }
635
+
636
+ function showResults() {
637
+ document.getElementById('results').style.display = 'block';
 
638
  }
639
+
640
+ function hideResults() {
641
+ document.getElementById('results').style.display = 'none';
642
  }
643
+
644
  function showError(message) {
645
+ const resultsDiv = document.getElementById('results');
646
+ resultsDiv.innerHTML = `<div class="error-message">❌ ${message}</div>`;
647
+ showResults();
648
  }
649
+
650
+ function showSuccess(message) {
651
+ const resultsDiv = document.getElementById('results');
652
+ const existingContent = resultsDiv.innerHTML;
653
+ resultsDiv.innerHTML = `<div class="success-message">✅ ${message}</div>` + existingContent;
654
+ showResults();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
  }
656
+
657
+ // Initialize the app
658
+ document.addEventListener('DOMContentLoaded', function() {
659
+ // Check API health on load
660
+ fetch(`${API_BASE}/health`)
661
+ .then(response => response.json())
662
+ .then(data => {
663
+ if (data.success) {
664
+ console.log('API is healthy:', data.data);
665
+ }
666
+ })
667
+ .catch(error => {
668
+ console.error('API health check failed:', error);
669
+ });
670
+ });
671
  </script>
672
  </body>
673
  </html>