furry/static/js/advanced-search.js

929 lines
31 KiB
JavaScript

/**
* Kasico Advanced Search Widget - Elasticsearch Integration
* Fuzzy Search, Faceted Search, Auto-Complete mit Furry-Design
*/
class KasicoAdvancedSearch {
constructor() {
this.currentQuery = '';
this.filters = {};
this.suggestions = [];
this.isSearching = false;
this.searchTimeout = null;
this.init();
}
init() {
// Search Widget HTML erstellen
this.createWidget();
// Event Listeners
this.bindEvents();
// Initialize search
this.initializeSearch();
}
createWidget() {
const widgetHTML = `
<div id="kasico-advanced-search" class="advanced-search-widget">
<!-- Search Header -->
<div class="search-header">
<div class="search-title">
<h2><i class="fas fa-search"></i> Erweiterte Suche</h2>
<p>Finde dein perfektes Fursuit mit unserer intelligenten Suche</p>
</div>
<div class="search-stats" id="search-stats">
<span class="results-count">0 Ergebnisse</span>
<span class="search-time">0ms</span>
</div>
</div>
<!-- Search Input -->
<div class="search-input-section">
<div class="search-input-wrapper">
<div class="search-input-group">
<i class="fas fa-search search-icon"></i>
<input type="text"
id="search-input"
class="search-input"
placeholder="Suche nach Fursuits, Auktionen, Kategorien..."
autocomplete="off">
<button class="search-clear" id="search-clear" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
<button class="search-button" id="search-button">
<i class="fas fa-search"></i>
Suchen
</button>
</div>
<!-- Search Suggestions -->
<div class="search-suggestions" id="search-suggestions" style="display: none;">
<div class="suggestions-header">
<i class="fas fa-lightbulb"></i>
<span>Vorschläge</span>
</div>
<div class="suggestions-list" id="suggestions-list">
</div>
</div>
</div>
<!-- Search Filters -->
<div class="search-filters">
<div class="filter-section">
<h4><i class="fas fa-filter"></i> Filter</h4>
<div class="filter-group">
<label>Kategorie</label>
<select id="category-filter" class="filter-select">
<option value="">Alle Kategorien</option>
<option value="fullsuit">Fullsuit</option>
<option value="partial">Partial</option>
<option value="head">Head Only</option>
<option value="handpaws">Handpaws</option>
<option value="feetpaws">Feetpaws</option>
<option value="tail">Tail</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="filter-group">
<label>Fursuit Typ</label>
<select id="fursuit-type-filter" class="filter-select">
<option value="">Alle Typen</option>
<option value="fullsuit">Fullsuit</option>
<option value="partial">Partial</option>
<option value="head">Head Only</option>
</select>
</div>
<div class="filter-group">
<label>Preisbereich</label>
<div class="price-range">
<input type="number"
id="price-min"
class="price-input"
placeholder="Min">
<span>-</span>
<input type="number"
id="price-max"
class="price-input"
placeholder="Max">
</div>
</div>
<div class="filter-group">
<label class="checkbox-label">
<input type="checkbox" id="featured-filter">
<span class="checkmark"></span>
Nur Featured Produkte
</label>
</div>
<div class="filter-group">
<label>Sortierung</label>
<select id="sort-filter" class="filter-select">
<option value="-created_at">Neueste zuerst</option>
<option value="price">Preis: Niedrig zu Hoch</option>
<option value="-price">Preis: Hoch zu Niedrig</option>
<option value="title">Name: A-Z</option>
<option value="-title">Name: Z-A</option>
</select>
</div>
</div>
<div class="filter-actions">
<button class="filter-clear" id="filter-clear">
<i class="fas fa-undo"></i>
Filter zurücksetzen
</button>
<button class="filter-apply" id="filter-apply">
<i class="fas fa-check"></i>
Filter anwenden
</button>
</div>
</div>
<!-- Search Results -->
<div class="search-results" id="search-results">
<div class="results-header">
<div class="results-info">
<span class="results-count">0 Ergebnisse gefunden</span>
<span class="search-query" id="search-query"></span>
</div>
<div class="results-view">
<button class="view-toggle active" data-view="grid">
<i class="fas fa-th"></i>
</button>
<button class="view-toggle" data-view="list">
<i class="fas fa-list"></i>
</button>
</div>
</div>
<div class="results-container" id="results-container">
<div class="empty-state">
<i class="fas fa-search"></i>
<h3>Starte deine Suche</h3>
<p>Gib einen Suchbegriff ein oder verwende die Filter</p>
</div>
</div>
<div class="results-pagination" id="results-pagination" style="display: none;">
</div>
</div>
<!-- Search Facets -->
<div class="search-facets" id="search-facets" style="display: none;">
<div class="facets-header">
<h4><i class="fas fa-tags"></i> Kategorien</h4>
</div>
<div class="facets-content" id="facets-content">
</div>
</div>
</div>
`;
// Widget in die Seite einfügen
const container = document.getElementById('search-container') || document.body;
container.insertAdjacentHTML('beforeend', widgetHTML);
// CSS hinzufügen
this.addStyles();
}
addStyles() {
const styles = `
.advanced-search-widget {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
padding: 30px;
color: white;
font-family: 'Quicksand', sans-serif;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
margin: 20px 0;
}
.search-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
}
.search-title h2 {
margin: 0 0 5px 0;
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 12px;
}
.search-title p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.search-stats {
display: flex;
gap: 20px;
font-size: 14px;
opacity: 0.8;
}
.search-input-section {
margin-bottom: 25px;
position: relative;
}
.search-input-wrapper {
display: flex;
gap: 15px;
align-items: center;
}
.search-input-group {
position: relative;
flex: 1;
}
.search-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #666;
font-size: 18px;
}
.search-input {
width: 100%;
padding: 15px 15px 15px 50px;
border: none;
border-radius: 25px;
font-size: 16px;
background: white;
color: #333;
outline: none;
transition: all 0.3s ease;
}
.search-input:focus {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
}
.search-clear {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 16px;
transition: color 0.3s ease;
}
.search-clear:hover {
color: #333;
}
.search-button {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #333;
border: none;
border-radius: 25px;
padding: 15px 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.search-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
.search-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
}
.suggestions-header {
padding: 15px 20px;
background: #f8f9fa;
border-radius: 15px 15px 0 0;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.suggestions-list {
padding: 0;
}
.suggestion-item {
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
gap: 12px;
color: #333;
}
.suggestion-item:hover {
background: #f8f9fa;
}
.suggestion-item.selected {
background: #e3f2fd;
}
.suggestion-icon {
width: 20px;
text-align: center;
color: #666;
}
.suggestion-text {
flex: 1;
}
.suggestion-type {
font-size: 12px;
opacity: 0.7;
padding: 2px 8px;
border-radius: 10px;
background: #f0f0f0;
}
.search-filters {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
}
.filter-section h4 {
margin: 0 0 20px 0;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.filter-group {
margin-bottom: 20px;
}
.filter-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
}
.filter-select {
width: 100%;
padding: 12px 15px;
border: none;
border-radius: 10px;
background: white;
color: #333;
font-size: 14px;
outline: none;
}
.price-range {
display: flex;
gap: 10px;
align-items: center;
}
.price-input {
flex: 1;
padding: 12px 15px;
border: none;
border-radius: 10px;
background: white;
color: #333;
font-size: 14px;
outline: none;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 14px;
}
.checkbox-label input[type="checkbox"] {
display: none;
}
.checkmark {
width: 20px;
height: 20px;
border: 2px solid white;
border-radius: 5px;
position: relative;
transition: all 0.3s ease;
}
.checkbox-label input[type="checkbox"]:checked + .checkmark {
background: #FFD700;
border-color: #FFD700;
}
.checkbox-label input[type="checkbox"]:checked + .checkmark::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #333;
font-weight: bold;
}
.filter-actions {
display: flex;
gap: 15px;
margin-top: 20px;
}
.filter-clear, .filter-apply {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.filter-clear {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.filter-apply {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #333;
}
.filter-clear:hover, .filter-apply:hover {
transform: translateY(-1px);
}
.search-results {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 25px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.results-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.results-count {
font-weight: 600;
font-size: 16px;
}
.search-query {
font-size: 14px;
opacity: 0.8;
}
.results-view {
display: flex;
gap: 5px;
}
.view-toggle {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.view-toggle.active {
background: #FFD700;
color: #333;
}
.results-container {
min-height: 200px;
}
.empty-state {
text-align: center;
padding: 40px;
opacity: 0.7;
}
.empty-state i {
font-size: 48px;
margin-bottom: 15px;
}
.empty-state h3 {
margin: 0 0 10px 0;
font-size: 20px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
.search-facets {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-top: 20px;
}
.facets-header h4 {
margin: 0 0 15px 0;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.facet-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
cursor: pointer;
transition: opacity 0.3s ease;
}
.facet-item:hover {
opacity: 0.8;
}
.facet-name {
font-size: 14px;
}
.facet-count {
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
/* Responsive Design */
@media (max-width: 768px) {
.advanced-search-widget {
padding: 20px;
}
.search-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.search-input-wrapper {
flex-direction: column;
}
.filter-actions {
flex-direction: column;
}
.results-header {
flex-direction: column;
gap: 15px;
}
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
bindEvents() {
// Search input
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
this.handleSearchInput(e.target.value);
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performSearch();
}
});
// Search button
document.getElementById('search-button').addEventListener('click', () => {
this.performSearch();
});
// Clear button
document.getElementById('search-clear').addEventListener('click', () => {
this.clearSearch();
});
// Filter events
document.getElementById('filter-apply').addEventListener('click', () => {
this.applyFilters();
});
document.getElementById('filter-clear').addEventListener('click', () => {
this.clearFilters();
});
// View toggles
document.querySelectorAll('.view-toggle').forEach(button => {
button.addEventListener('click', (e) => {
this.toggleView(e.target.closest('.view-toggle').dataset.view);
});
});
}
handleSearchInput(query) {
this.currentQuery = query;
// Show/hide clear button
const clearButton = document.getElementById('search-clear');
clearButton.style.display = query ? 'block' : 'none';
// Clear previous timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Debounce search suggestions
if (query.length >= 2) {
this.searchTimeout = setTimeout(() => {
this.getSuggestions(query);
}, 300);
} else {
this.hideSuggestions();
}
}
async getSuggestions(query) {
try {
const response = await fetch(`/search/suggestions/?q=${encodeURIComponent(query)}`);
const data = await response.json();
this.displaySuggestions(data.suggestions);
} catch (error) {
console.error('Error fetching suggestions:', error);
}
}
displaySuggestions(suggestions) {
const suggestionsContainer = document.getElementById('search-suggestions');
const suggestionsList = document.getElementById('suggestions-list');
if (suggestions.length === 0) {
suggestionsContainer.style.display = 'none';
return;
}
suggestionsList.innerHTML = suggestions.map(suggestion => `
<div class="suggestion-item" data-query="${suggestion.title}">
<div class="suggestion-icon">
<i class="fas fa-${suggestion.type === 'product' ? 'tshirt' : 'gavel'}"></i>
</div>
<div class="suggestion-text">${suggestion.title}</div>
<div class="suggestion-type">${suggestion.type}</div>
</div>
`).join('');
// Add click events
suggestionsList.querySelectorAll('.suggestion-item').forEach(item => {
item.addEventListener('click', () => {
const query = item.dataset.query;
document.getElementById('search-input').value = query;
this.currentQuery = query;
this.hideSuggestions();
this.performSearch();
});
});
suggestionsContainer.style.display = 'block';
}
hideSuggestions() {
document.getElementById('search-suggestions').style.display = 'none';
}
async performSearch() {
if (!this.currentQuery.trim()) {
return;
}
this.isSearching = true;
this.showLoading();
try {
const startTime = performance.now();
const response = await fetch('/search/api/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
params: {
q: this.currentQuery,
filters: JSON.stringify(this.filters),
}
});
const data = await response.json();
const endTime = performance.now();
this.displayResults(data.results, endTime - startTime);
this.updateSearchStats(data.total, endTime - startTime);
} catch (error) {
console.error('Search error:', error);
this.showError('Fehler bei der Suche');
} finally {
this.isSearching = false;
this.hideLoading();
}
}
displayResults(results, searchTime) {
const container = document.getElementById('results-container');
if (results.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-search"></i>
<h3>Keine Ergebnisse gefunden</h3>
<p>Versuche andere Suchbegriffe oder Filter</p>
</div>
`;
return;
}
container.innerHTML = results.map(result => `
<div class="result-item">
<div class="result-image">
<img src="${result.image_url || '/static/images/placeholder.jpg'}" alt="${result.title}">
</div>
<div class="result-content">
<h4>${result.title}</h4>
<p>${result.description}</p>
<div class="result-meta">
<span class="result-price">€${result.price}</span>
<span class="result-type">${result.type}</span>
</div>
</div>
</div>
`).join('');
}
updateSearchStats(total, searchTime) {
document.getElementById('search-query').textContent = `für "${this.currentQuery}"`;
document.querySelector('.results-count').textContent = `${total} Ergebnisse`;
document.querySelector('.search-time').textContent = `${Math.round(searchTime)}ms`;
}
applyFilters() {
this.filters = {
category: document.getElementById('category-filter').value,
fursuit_type: document.getElementById('fursuit-type-filter').value,
price_min: document.getElementById('price-min').value,
price_max: document.getElementById('price-max').value,
featured: document.getElementById('featured-filter').checked,
sort_by: document.getElementById('sort-filter').value,
};
this.performSearch();
}
clearFilters() {
document.getElementById('category-filter').value = '';
document.getElementById('fursuit-type-filter').value = '';
document.getElementById('price-min').value = '';
document.getElementById('price-max').value = '';
document.getElementById('featured-filter').checked = false;
document.getElementById('sort-filter').value = '-created_at';
this.filters = {};
}
clearSearch() {
document.getElementById('search-input').value = '';
this.currentQuery = '';
document.getElementById('search-clear').style.display = 'none';
this.hideSuggestions();
// Reset results
document.getElementById('results-container').innerHTML = `
<div class="empty-state">
<i class="fas fa-search"></i>
<h3>Starte deine Suche</h3>
<p>Gib einen Suchbegriff ein oder verwende die Filter</p>
</div>
`;
}
toggleView(view) {
document.querySelectorAll('.view-toggle').forEach(btn => {
btn.classList.remove('active');
});
event.target.closest('.view-toggle').classList.add('active');
const container = document.getElementById('results-container');
container.className = `results-container view-${view}`;
}
showLoading() {
const container = document.getElementById('results-container');
container.innerHTML = `
<div class="loading-state">
<i class="fas fa-spinner fa-spin"></i>
<p>Suchen...</p>
</div>
`;
}
hideLoading() {
// Loading state will be replaced by results
}
showError(message) {
const container = document.getElementById('results-container');
container.innerHTML = `
<div class="error-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Fehler</h3>
<p>${message}</p>
</div>
`;
}
initializeSearch() {
// Initialize with empty state
this.clearSearch();
}
}
// Advanced Search Widget initialisieren
document.addEventListener('DOMContentLoaded', function() {
window.kasicoAdvancedSearch = new KasicoAdvancedSearch();
});