Spaces:
Running
Running
<html lang="{{ current_language }}"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Agricultural Pests & Diseases in India</title> | |
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
<style> | |
:root { | |
--primary-color: #38b000; | |
--secondary-color: #007bff; | |
--accent-color: #fb8500; | |
--light-bg: #f8f9fa; | |
--dark-text: #212529; | |
--pest-color: #dc3545; | |
--disease-color: #6f42c1; | |
} | |
body { | |
background-color: var(--light-bg); | |
padding: 20px; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.page-header { | |
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
color: white; | |
padding: 2rem 0; | |
border-radius: 10px; | |
margin-bottom: 2rem; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
} | |
.language-selector select { | |
background-color: rgba(255, 255, 255, 0.2); | |
color: white; | |
border: 1px solid rgba(255, 255, 255, 0.3); | |
} | |
.language-selector select option { | |
background-color: white; | |
color: var(--dark-text); | |
} | |
.filter-buttons { margin-bottom: 20px; } | |
.filter-btn { | |
margin-right: 10px; | |
border-radius: 20px; | |
font-weight: 500; | |
padding: 8px 16px; | |
transition: all 0.3s ease; | |
} | |
.filter-btn:hover { transform: translateY(-2px); } | |
.filter-btn.active { box-shadow: 0 4px 8px rgba(0,0,0,0.15); } | |
.card { transition: all 0.3s ease; height: 100%; cursor: pointer; border-radius: 10px; border: none; box-shadow: 0 4px 8px rgba(0,0,0,0.05); overflow: hidden; } | |
.card:hover { transform: translateY(-8px); box-shadow: 0 12px 24px rgba(0,0,0,0.15); } | |
.card-img-top { height: 200px; object-fit: cover; transition: all 0.5s ease; } | |
.card:hover .card-img-top { transform: scale(1.05); } | |
.card-body { padding: 1.5rem; } | |
.card-title { font-weight: 600; margin-bottom: 10px; } | |
.modal-body { max-height: 70vh; overflow-y: auto; } | |
.badge.pest { background-color: var(--pest-color); padding: 0.5em 0.8em; font-size: 0.8rem; } | |
.badge.disease { background-color: var(--disease-color); padding: 0.5em 0.8em; font-size: 0.8rem; } | |
.modal-content { border-radius: 15px; border: none; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } | |
.modal-header { border-bottom: 1px solid rgba(0,0,0,0.1); background-color: var(--light-bg); } | |
.modal-title { font-weight: 600; } | |
.section-title { | |
margin-top: 1.5rem; | |
margin-bottom: 0.75rem; | |
font-weight: 600; | |
color: var(--dark-text); | |
border-bottom: 2px solid var(--primary-color); | |
padding-bottom: 0.5rem; | |
display: inline-block; | |
} | |
#backToTop { position: fixed; bottom: 20px; right: 20px; display: none; background-color: var(--primary-color); color: white; border: none; border-radius: 50%; width: 50px; height: 50px; text-align: center; font-size: 20px; line-height: 50px; cursor: pointer; z-index: 99; box-shadow: 0 4px 10px rgba(0,0,0,0.2); } | |
.crop-badge { background-color: #17a2b8; color: white; font-size: 0.8rem; font-weight: normal; margin-right: 4px; } | |
.no-results { text-align: center; padding: 40px; font-size: 1.2rem; color: #6c757d; } | |
.spinner { | |
width: 40px; height: 40px; margin: 20px auto; border: 4px solid rgba(0,0,0,0.1); border-left-color: var(--primary-color); border-radius: 50%; animation: spin 1s linear infinite; | |
} | |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<header class="page-header text-center mb-4"> | |
<h1 class="display-4">Agricultural Pests & Diseases in India</h1> | |
<p class="lead">Explore common agricultural threats and learn how to manage them</p> | |
<div class="language-selector mt-3"> | |
<form id="languageForm" method="POST" action="{{ url_for('set_language') }}" class="d-flex justify-content-center align-items-center"> | |
<label for="languageSelect" class="me-2 text-white"><i class="fas fa-language"></i> Language:</label> | |
<select id="languageSelect" name="language" class="form-select form-select-sm" style="max-width: 220px;" onchange="this.form.submit()"> | |
{% for code, name in languages.items() %} | |
<option value="{{ code }}" {% if code == current_language %}selected{% endif %}>{{ name }}</option> | |
{% endfor %} | |
</select> | |
</form> | |
</div> | |
</header> | |
<div class="filter-buttons text-center mb-4"> | |
<button class="btn btn-outline-primary filter-btn active" data-filter="all">All Items</button> | |
<button class="btn btn-outline-danger filter-btn" data-filter="pest">Pests Only</button> | |
<button class="btn btn-outline-purple filter-btn" data-filter="disease" style="border-color: #6f42c1; color: #6f42c1;">Diseases Only</button> | |
<div class="input-group mt-3 w-50 mx-auto"> | |
<span class="input-group-text"><i class="fas fa-search"></i></span> | |
<input type="text" class="form-control" id="searchInput" placeholder="Search by name or crop..."> | |
</div> | |
</div> | |
<div class="row row-cols-1 row-cols-md-3 g-4" id="itemsContainer"> | |
{% for item in pests_diseases %} | |
<div class="col item-card" data-type="{{ item.type }}" data-name="{{ item.name.lower() }}" data-crop="{{ item.crop.lower() }}"> | |
<div class="card h-100" onclick="showDetails({{ item.id }})"> | |
<div class="position-relative overflow-hidden"> | |
<img data-src="{{ item.image_url }}" class="card-img-top lazy-load" alt="{{ item.name }}"> | |
<span class="position-absolute top-0 end-0 m-2 badge {{ item.type }}">{{ item.type|capitalize }}</span> | |
</div> | |
<div class="card-body"> | |
<h5 class="card-title">{{ item.name }}</h5> | |
<p class="card-text"> | |
<strong>Affects:</strong><br> | |
{% for crop in item.crop.split(', ') %} | |
<span class="badge crop-badge">{{ crop }}</span> | |
{% endfor %} | |
</p> | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
<div id="noResults" class="no-results" style="display: none;"> | |
<i class="fas fa-search fa-3x mb-3 text-muted"></i> | |
<p>No matching items found. Try another search term.</p> | |
</div> | |
<!-- Detail Modal --> | |
<div class="modal fade" id="detailModal" tabindex="-1" aria-hidden="true"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title" id="modalTitle"></h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body" id="modalBody"> | |
<div class="text-center p-5" id="modalLoading"> | |
<div class="spinner"></div><p class="mt-3">Loading content...</p> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<button id="backToTop" title="Back to top"><i class="fas fa-chevron-up"></i></button> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> | |
<script> | |
const detailModal = new bootstrap.Modal(document.getElementById('detailModal')); | |
let loadedDetails = {}; | |
document.getElementById('languageSelect').addEventListener('change', function() { | |
// Clear client-side cache to force reload in new language | |
loadedDetails = {}; | |
}); | |
// Lazy load images | |
document.addEventListener("DOMContentLoaded", function() { | |
const lazyImages = document.querySelectorAll(".lazy-load"); | |
if ("IntersectionObserver" in window) { | |
const imageObserver = new IntersectionObserver(function(entries, observer) { | |
entries.forEach(function(entry) { | |
if (entry.isIntersecting) { | |
const img = entry.target; | |
img.src = img.dataset.src; | |
img.classList.remove("lazy-load"); | |
imageObserver.unobserve(img); | |
} | |
}); | |
}); | |
lazyImages.forEach(function(image) { imageObserver.observe(image); }); | |
} else { | |
lazyImages.forEach(function(img) { img.src = img.dataset.src; }); | |
} | |
}); | |
// Filter & search logic | |
const searchInput = document.getElementById('searchInput'); | |
const filterButtons = document.querySelectorAll('.filter-btn'); | |
function filterItems() { | |
const searchTerm = searchInput.value.toLowerCase(); | |
const activeFilter = document.querySelector('.filter-btn.active').getAttribute('data-filter'); | |
let visibleCount = 0; | |
document.querySelectorAll('.item-card').forEach(item => { | |
const type = item.getAttribute('data-type'); | |
const name = item.getAttribute('data-name'); | |
const crop = item.getAttribute('data-crop'); | |
const typeMatch = activeFilter === 'all' || type === activeFilter; | |
const searchMatch = !searchTerm || name.includes(searchTerm) || crop.includes(searchTerm); | |
if (typeMatch && searchMatch) { | |
item.style.display = 'block'; | |
visibleCount++; | |
} else { | |
item.style.display = 'none'; | |
} | |
}); | |
document.getElementById('noResults').style.display = visibleCount === 0 ? 'block' : 'none'; | |
} | |
filterButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
filterButtons.forEach(btn => btn.classList.remove('active')); | |
this.classList.add('active'); | |
filterItems(); | |
}); | |
}); | |
searchInput.addEventListener('input', filterItems); | |
// Show details modal | |
function showDetails(id) { | |
document.getElementById('modalTitle').textContent = ""; | |
document.getElementById('modalBody').innerHTML = `<div class="text-center p-5" id="modalLoading"><div class="spinner"></div><p class="mt-3">Loading content...</p></div>`; | |
detailModal.show(); | |
const currentLanguage = document.getElementById('languageSelect').value; | |
const cacheKey = `${id}-${currentLanguage}`; | |
if (loadedDetails[cacheKey]) { | |
displayDetails(loadedDetails[cacheKey]); | |
return; | |
} | |
fetch(`/get_details/${id}`) | |
.then(response => { | |
if (!response.ok) { throw new Error(`Network response was not ok (${response.status})`); } | |
return response.json(); | |
}) | |
.then(data => { | |
if (data.error) { throw new Error(data.message || data.error); } | |
loadedDetails[cacheKey] = data; | |
displayDetails(data); | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
document.getElementById('modalBody').innerHTML = `<div class="alert alert-danger" role="alert"><i class="fas fa-exclamation-circle"></i> Error loading information. Please check the server logs and ensure the API key is valid.</div>`; | |
}); | |
} | |
function displayDetails(data) { | |
document.getElementById('modalTitle').textContent = data.name; | |
const details = data.details; | |
let content = ` | |
<div class="text-center mb-4"> | |
<img src="${data.image_url}" alt="${data.name}" class="img-fluid rounded" style="max-height: 300px;"> | |
<div class="mt-2"> | |
<span class="badge ${data.type} mb-2">${data.type.charAt(0).toUpperCase() + data.type.slice(1)}</span> | |
<div><strong>Affects:</strong> ${data.crop.split(', ').map(crop => `<span class="badge crop-badge">${crop}</span>`).join(' ')}</div> | |
</div> | |
</div>`; | |
const sections = [ | |
{key: 'description', icon: 'info-circle', title: details.description.title || 'Description'}, | |
{key: 'lifecycle', icon: 'sync', title: details.lifecycle.title || 'Lifecycle'}, | |
{key: 'symptoms', icon: 'exclamation-triangle', title: details.symptoms.title || 'Symptoms'}, | |
{key: 'impact', icon: 'chart-line', title: details.impact.title || 'Economic Impact'}, | |
{key: 'management', icon: 'tasks', title: details.management.title || 'Management'}, | |
{key: 'prevention', icon: 'shield-alt', title: details.prevention.title || 'Prevention'} | |
]; | |
sections.forEach(section => { | |
if (details[section.key] && details[section.key].text) { | |
content += `<h4 class="section-title"><i class="fas fa-${section.icon} me-2"></i>${section.title}</h4><p>${details[section.key].text.replace(/\n/g, '<br>')}</p>`; | |
} | |
}); | |
document.getElementById('modalBody').innerHTML = content; | |
} | |
// Back to top | |
const backToTopButton = document.getElementById("backToTop"); | |
window.onscroll = () => { | |
backToTopButton.style.display = (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) ? "block" : "none"; | |
}; | |
backToTopButton.addEventListener("click", () => { | |
window.scrollTo({top: 0, behavior: 'smooth'}); | |
}); | |
</script> | |
</body> | |
</html> |