evwiki/templates/_components/search.html.twig
2025-06-09 18:15:22 +02:00

389 lines
12 KiB
Twig

<div class="search-container" id="searchForm">
<input type="text" id="searchInput" class="search-input" placeholder="🔍 Search for electric vehicles, brands, or models..." value="{{ query|default('') }}">
<button type="button" id="searchButton" class="search-button">
<span class="search-btn-content"><i class="fas fa-search"></i> Search</span>
<span class="search-btn-spinner" style="display:none;"></span>
</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.getElementById('searchForm');
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const resultsContainer = document.getElementById('resultsContainer');
function encodeSearchQuery(query) {
return encodeURIComponent(query);
}
function setLoading(isLoading) {
if (isLoading) {
searchForm.classList.add('loading');
searchButton.querySelector('.search-btn-content').style.display = 'none';
searchButton.querySelector('.search-btn-spinner').style.display = 'inline-block';
if (resultsContainer) {
if (resultsContainer.innerHTML.trim()) {
// If there is already content, just fade it
resultsContainer.classList.add('loading-fade');
} else {
// If no content, show placeholders
fadeOutIn(resultsContainer, `
<div class="placeholder-glow placeholder-detail-layout">
<div class="placeholder-header">
<div class="placeholder placeholder-title-main"></div>
<div class="placeholder placeholder-title-sub"></div>
</div>
<div class="placeholder-content-row">
<div class="placeholder-image-col">
<div class="placeholder placeholder-large-img"></div>
</div>
<div class="placeholder-info-col">
<div class="placeholder-info-grid">
<div class="placeholder-info-block">
<div class="placeholder placeholder-icon"></div>
<div class="placeholder placeholder-value"></div>
<div class="placeholder placeholder-label"></div>
</div>
<div class="placeholder-info-block">
<div class="placeholder placeholder-icon"></div>
<div class="placeholder placeholder-value"></div>
<div class="placeholder placeholder-label"></div>
</div>
<div class="placeholder-info-block">
<div class="placeholder placeholder-icon"></div>
<div class="placeholder placeholder-value"></div>
<div class="placeholder placeholder-label"></div>
</div>
<div class="placeholder-info-block">
<div class="placeholder placeholder-icon"></div>
<div class="placeholder placeholder-value"></div>
<div class="placeholder placeholder-label"></div>
</div>
</div>
</div>
</div>
</div>
`, () => {
resultsContainer.style.display = 'block';
});
}
}
} else {
searchForm.classList.remove('loading');
// Hide spinner, show button content
searchButton.querySelector('.search-btn-content').style.display = 'inline-block';
searchButton.querySelector('.search-btn-spinner').style.display = 'none';
if (resultsContainer) {
resultsContainer.classList.remove('loading-fade');
}
}
}
function fadeOutIn(element, newContent, callback) {
element.classList.add('fade-out');
setTimeout(() => {
element.innerHTML = newContent;
element.classList.remove('fade-out');
element.classList.add('fade-in');
setTimeout(() => {
element.classList.remove('fade-in');
if (callback) callback();
}, 150); // match the CSS transition duration
}, 150); // match the CSS transition duration
}
function performSearch(query) {
if (!query.trim()) {
return;
}
const encodedQuery = encodeSearchQuery(query);
// Show loading animation on search bar
setLoading(true);
// Update URL without page reload
const newUrl = `/s/${encodedQuery}`;
window.history.pushState({ query: query }, '', newUrl);
// Make AJAX request to get results
fetch(`/result/${encodedQuery}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
// Hide loading animation
setLoading(false);
fadeOutIn(resultsContainer, html, () => {
resultsContainer.style.display = 'block';
});
})
.catch(error => {
console.error('Search error:', error);
// Hide loading animation
setLoading(false);
// Show error message
fadeOutIn(resultsContainer, '<div class="no-results">Sorry, there was an error performing your search. Please try again.</div>', () => {
resultsContainer.style.display = 'block';
});
});
}
// Handle form submission
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
const query = searchInput.value.trim();
performSearch(query);
});
// Handle search button click
searchButton.addEventListener('click', function(e) {
e.preventDefault();
const query = searchInput.value.trim();
performSearch(query);
});
// Handle Enter key press
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const query = searchInput.value.trim();
performSearch(query);
}
});
// Handle browser back/forward buttons
window.addEventListener('popstate', function(event) {
if (event.state && event.state.query) {
searchInput.value = event.state.query;
performSearch(event.state.query);
}
});
// Check for initial results on the page
const initialResults = document.getElementById('initialResults');
const initialQuery = searchInput.value.trim();
if (initialResults && initialResults.innerHTML.trim()) {
// Show initial results if they exist
resultsContainer.innerHTML = initialResults.innerHTML;
resultsContainer.style.display = 'block';
} else if (initialQuery) {
// If there's a query but no initial results, perform search
performSearch(initialQuery);
}
});
</script>
<style>
.search-container.loading {
position: relative;
}
.no-results {
text-align: center;
padding: 2rem;
color: #999;
font-style: italic;
}
.search-btn-spinner {
width: 1.2em;
height: 1.2em;
border: 2.5px solid #e0e0e0;
border-top: 2.5px solid #083d77;
border-radius: 50%;
animation: search-btn-spin 0.8s linear infinite;
vertical-align: middle;
margin-left: 0.2em;
display: none;
}
@keyframes search-btn-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.placeholder-glow {
display: block;
animation: placeholder-glow 2s ease-in-out infinite;
}
@keyframes placeholder-glow {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.placeholder {
display: block;
background-color:rgb(198, 198, 198);
height: 1.2rem;
width: 100%;
vertical-align: middle;
margin-left: 0.2em;
margin-bottom: 0.5rem;
}
.placeholder.col-6 {
width: 60%;
}
.placeholder.col-7 {
width: 70%;
}
.placeholder.col-4 {
width: 40%;
}
.placeholder.col-8 {
width: 80%;
}
.placeholder-tiles {
display: flex;
gap: 2rem;
justify-content: stretch;
margin-top: 2rem;
}
.placeholder-tile {
background: #f6f6f6;
padding: 1.5rem 1.2rem 1.2rem 1.2rem;
width: 260px;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 320px;
}
.placeholder-img {
width: 90px;
height: 90px;
background-color: #d2d2d2;
margin-bottom: 1.2rem;
}
.placeholder-title {
width: 70%;
height: 1.3rem;
margin-bottom: 0.7rem;
}
.placeholder-subtitle {
width: 50%;
height: 1rem;
margin-bottom: 1.1rem;
}
.placeholder-spec {
width: 80%;
height: 0.9rem;
margin-bottom: 0.5rem;
}
.placeholder-spec.short {
width: 40%;
}
.placeholder-btn {
width: 60%;
height: 1.2rem;
border-radius: 0.6rem;
margin-top: 1.2rem;
}
.placeholder-detail-layout {
display: flex;
flex-direction: column;
gap: 2.5rem;
margin-top: 2rem;
width: 100%;
}
.placeholder-header {
margin-bottom: 0.5rem;
}
.placeholder-title-main {
width: 320px;
height: 2.2rem;
margin-bottom: 0.7rem;
}
.placeholder-title-sub {
width: 220px;
height: 1.2rem;
margin-bottom: 0.7rem;
}
.placeholder-content-row {
display: flex;
flex-direction: row;
gap: 2.5rem;
width: 100%;
}
.placeholder-image-col {
flex: 1.2;
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.placeholder-large-img {
width: 100%;
max-width: 520px;
height: 260px;
border-radius: 0.7rem;
background-color: #d2d2d2;
}
.placeholder-info-col {
flex: 1.5;
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.placeholder-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 2.2rem 2.5rem;
width: 100%;
}
.placeholder-info-block {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.placeholder-icon {
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
background-color: #e0e0e0;
margin-bottom: 0.3rem;
}
.placeholder-value {
width: 90px;
height: 1.3rem;
margin-bottom: 0.2rem;
}
.placeholder-label {
width: 120px;
height: 0.9rem;
background-color: #eaeaea;
}
#resultsContainer {
opacity: 1;
transition: opacity 0.15s ease;
}
#resultsContainer.fade-out {
opacity: 0;
pointer-events: none;
}
#resultsContainer.fade-in {
opacity: 1;
}
/* Add fade for loading existing content */
#resultsContainer.loading-fade {
opacity: 0.3;
transition: opacity 0.3s ease;
}
</style>