PhoneArena / templates /index.html
NitinBot001's picture
Create templates/index.html
6333fb8 verified
raw
history blame
25.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phone Specifications Search</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
color: white;
}
.header h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
}
.search-container {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.search-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-weight: 600;
color: #555;
}
.search-input {
padding: 15px;
border: 2px solid #e1e5e9;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
}
.source-selector {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.source-option {
display: flex;
align-items: center;
gap: 8px;
}
.source-option input[type="radio"] {
accent-color: #667eea;
}
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102,126,234,0.3);
}
.btn-secondary {
background: #f8f9fa;
color: #495057;
border: 2px solid #e9ecef;
}
.btn-secondary:hover {
background: #e9ecef;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results-container {
display: none;
}
.phone-card {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.phone-card:hover {
transform: translateY(-5px);
}
.phone-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.phone-title {
font-size: 1.8rem;
font-weight: 700;
color: #333;
}
.phone-brand {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
}
.phone-images {
display: flex;
gap: 15px;
margin-bottom: 20px;
overflow-x: auto;
padding-bottom: 10px;
}
.phone-image {
width: 150px;
height: 200px;
object-fit: cover;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.specs-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.spec-category {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
}
.spec-category h3 {
color: #495057;
margin-bottom: 15px;
font-size: 1.2rem;
border-bottom: 2px solid #dee2e6;
padding-bottom: 5px;
}
.spec-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.spec-item:last-child {
border-bottom: none;
}
.spec-label {
font-weight: 600;
color: #495057;
}
.spec-value {
color: #6c757d;
text-align: right;
max-width: 60%;
}
.multiple-phones-section {
background: white;
border-radius: 20px;
padding: 30px;
margin-top: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.multiple-phones-section h2 {
margin-bottom: 20px;
color: #333;
}
.phone-list-input {
width: 100%;
min-height: 100px;
padding: 15px;
border: 2px solid #e1e5e9;
border-radius: 10px;
font-size: 16px;
resize: vertical;
margin-bottom: 20px;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-healthy { background-color: #28a745; }
.status-error { background-color: #dc3545; }
.alert {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.export-section {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #e9ecef;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.search-container,
.multiple-phones-section {
padding: 20px;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
.phone-header {
flex-direction: column;
align-items: flex-start;
}
.specs-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📱 Phone Specifications Search</h1>
<p>Search and compare phone specifications from multiple sources</p>
</div>
<!-- API Status -->
<div id="api-status" class="alert alert-success">
<span class="status-indicator status-healthy"></span>
API Status: Checking...
</div>
<!-- Single Phone Search -->
<div class="search-container">
<h2>Search Single Phone</h2>
<form class="search-form" id="single-search-form">
<div class="input-group">
<label for="phone-name">Phone Name</label>
<input
type="text"
id="phone-name"
class="search-input"
placeholder="e.g., iPhone 15 Pro, Samsung Galaxy S24, OnePlus 12"
required
>
</div>
<div class="input-group">
<label>Data Source</label>
<div class="source-selector">
<div class="source-option">
<input type="radio" id="gsmarena" name="source" value="gsmarena" checked>
<label for="gsmarena">GSMArena</label>
</div>
<div class="source-option">
<input type="radio" id="phonedb" name="source" value="phonedb">
<label for="phonedb">PhoneDB</label>
</div>
</div>
</div>
<div class="button-group">
<button type="submit" class="btn btn-primary">
🔍 Search Phone
</button>
<button type="button" class="btn btn-secondary" onclick="clearResults()">
🗑️ Clear Results
</button>
</div>
</form>
</div>
<!-- Loading indicator -->
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Searching for phone specifications...</p>
</div>
<!-- Results container -->
<div id="results" class="results-container"></div>
<!-- Multiple Phones Search -->
<div class="multiple-phones-section">
<h2>Search Multiple Phones</h2>
<p style="margin-bottom: 20px; color: #6c757d;">Enter phone names separated by new lines (one phone per line)</p>
<textarea
id="phone-list"
class="phone-list-input"
placeholder="iPhone 15 Pro&#10;Samsung Galaxy S24&#10;OnePlus 12&#10;Google Pixel 8"
></textarea>
<div class="input-group">
<label>Data Source</label>
<div class="source-selector">
<div class="source-option">
<input type="radio" id="gsmarena-multi" name="source-multi" value="gsmarena" checked>
<label for="gsmarena-multi">GSMArena</label>
</div>
<div class="source-option">
<input type="radio" id="phonedb-multi" name="source-multi" value="phonedb">
<label for="phonedb-multi">PhoneDB</label>
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="searchMultiplePhones()">
🔍 Search All Phones
</button>
<button class="btn btn-secondary" onclick="startBackgroundSearch()">
⏱️ Background Search
</button>
</div>
</div>
</div>
<script>
const API_BASE = window.location.origin;
// Check API health on load
window.addEventListener('load', checkApiHealth);
async function checkApiHealth() {
try {
const response = await fetch(`${API_BASE}/health`);
const data = await response.json();
const statusEl = document.getElementById('api-status');
if (data.success) {
statusEl.className = 'alert alert-success';
statusEl.innerHTML = `
<span class="status-indicator status-healthy"></span>
API Status: Healthy (GSMArena: ${data.data.scrapers.gsmarena ? '✅' : '❌'}, PhoneDB: ${data.data.scrapers.phonedb ? '✅' : '❌'})
`;
} else {
throw new Error('API not healthy');
}
} catch (error) {
const statusEl = document.getElementById('api-status');
statusEl.className = 'alert alert-error';
statusEl.innerHTML = `
<span class="status-indicator status-error"></span>
API Status: Error - ${error.message}
`;
}
}
// Single phone search
document.getElementById('single-search-form').addEventListener('submit', async (e) => {
e.preventDefault();
const phoneName = document.getElementById('phone-name').value.trim();
const source = document.querySelector('input[name="source"]:checked').value;
if (!phoneName) {
alert('Please enter a phone name');
return;
}
showLoading(true);
clearResults();
try {
const response = await fetch(`${API_BASE}/api/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone_name: phoneName,
source: source
})
});
const data = await response.json();
if (data.success && data.data) {
displayPhoneResults([data.data]);
} else {
showError(data.message || 'No results found');
}
} catch (error) {
showError('Error searching for phone: ' + error.message);
} finally {
showLoading(false);
}
});
// Multiple phones search
async function searchMultiplePhones() {
const phoneList = document.getElementById('phone-list').value.trim();
const source = document.querySelector('input[name="source-multi"]:checked').value;
if (!phoneList) {
alert('Please enter phone names');
return;
}
const phoneNames = phoneList.split('\n')
.map(name => name.trim())
.filter(name => name.length > 0);
if (phoneNames.length === 0) {
alert('Please enter valid phone names');
return;
}
showLoading(true);
clearResults();
try {
const response = await fetch(`${API_BASE}/api/search/multiple`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone_names: phoneNames,
source: source
})
});
const data = await response.json();
if (data.success && data.data.phones.length > 0) {
displayPhoneResults(data.data.phones);
showSuccess(`Successfully found ${data.data.success_count}/${data.data.total_count} phones`);
} else {
showError(data.message || 'No results found');
}
} catch (error) {
showError('Error searching for phones: ' + error.message);
} finally {
showLoading(false);
}
}
// Background search
async function startBackgroundSearch() {
const phoneList = document.getElementById('phone-list').value.trim();
const source = document.querySelector('input[name="source-multi"]:checked').value;
if (!phoneList) {
alert('Please enter phone names');
return;
}
const phoneNames = phoneList.split('\n')
.map(name => name.trim())
.filter(name => name.length > 0);
try {
const response = await fetch(`${API_BASE}/api/scrape/background`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone_names: phoneNames,
source: source
})
});
const data = await response.json();
if (data.success) {
const jobId = data.data.job_id;
showSuccess(`Background job started: ${jobId}`);
monitorBackgroundJob(jobId);
} else {
showError('Failed to start background job');
}
} catch (error) {
showError('Error starting background job: ' + error.message);
}
}
// Monitor background job
async function monitorBackgroundJob(jobId) {
const checkStatus = async () => {
try {
const response = await fetch(`${API_BASE}/api/scrape/status/${jobId}`);
const data = await response.json();
if (data.success) {
const job = data.data;
const progress = `${job.progress}/${job.total}`;
if (job.status === 'completed') {
displayPhoneResults(job.results);
showSuccess(`Background job completed: ${progress} phones processed`);
return;
} else if (job.status === 'failed') {
showError(`Background job failed: ${job.error || 'Unknown error'}`);
return;
} else {
showSuccess(`Background job running: ${progress} phones processed${job.current_phone ? ` (Current: ${job.current_phone})` : ''}`);
setTimeout(checkStatus, 3000); // Check every 3 seconds
}
}
} catch (error) {
showError('Error monitoring job: ' + error.message);
}
};
checkStatus();
}
// Display results
function displayPhoneResults(phones) {
const resultsContainer = document.getElementById('results');
resultsContainer.innerHTML = '';
phones.forEach(phone => {
const phoneCard = createPhoneCard(phone);
resultsContainer.appendChild(phoneCard);
});
resultsContainer.style.display = 'block';
}
// Create phone card
function createPhoneCard(phone) {
const card = document.createElement('div');
card.className = 'phone-card';
// Images HTML
const imagesHtml = phone.images && phone.images.length > 0
? `<div class="phone-images">
${phone.images.map(img => `<img src="${img}" alt="${phone.name}" class="phone-image" onerror="this.style.display='none'">`).join('')}
</div>`
: '';
// Specifications HTML
const specsHtml = Object.entries(phone.specifications || {})
.map(([category, specs]) => {
if (typeof specs === 'object' && specs !== null) {
const specItems = Object.entries(specs)
.map(([key, value]) => `
<div class="spec-item">
<span class="spec-label">${key}</span>
<span class="spec-value">${value}</span>
</div>
`).join('');
return `
<div class="spec-category">
<h3>${category}</h3>
${specItems}
</div>
`;
}
return '';
}).join('');
card.innerHTML = `
<div class="phone-header">
<h2 class="phone-title">${phone.name}</h2>
<span class="phone-brand">${phone.brand}</span>
</div>
${imagesHtml}
<div class="specs-container">
${specsHtml}
</div>
<div class="export-section">
<button class="btn btn-secondary" onclick="exportPhoneData('${phone.name}')">
💾 Export JSON
</button>
<a href="${phone.source_url}" target="_blank" class="btn btn-secondary">
🔗 View Source
</a>
</div>
`;
return card;
}
// Export phone data
async function exportPhoneData(phoneName) {
try {
const source = document.querySelector('input[name="source"]:checked').value;
window.open(`${API_BASE}/api/export/${encodeURIComponent(phoneName)}?source=${source}`, '_blank');
} catch (error) {
showError('Error exporting data: ' + error.message);
}
}
// Utility functions
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function clearResults() {
document.getElementById('results').style.display = 'none';
document.getElementById('results').innerHTML = '';
}
function showSuccess(message) {
showAlert(message, 'success');
}
function showError(message) {
showAlert(message, 'error');
}
function showAlert(message, type) {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert:not(#api-status)');
existingAlerts.forEach(alert => alert.remove());
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
const container = document.querySelector('.container');
const apiStatus = document.getElementById('api-status');
container.insertBefore(alert, apiStatus.nextSibling);
// Auto remove after 5 seconds
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 5000);
}
</script>
</body>
</html>