404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
/**
|
|
* Recommendation Widget für Frontend Integration
|
|
* ML-basierte Empfehlungen mit Furry-Design
|
|
*/
|
|
|
|
class RecommendationWidget {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
container: options.container || '#recommendations',
|
|
apiEndpoint: options.apiEndpoint || '/recommendations/api/recommendations/',
|
|
behaviorEndpoint: options.behaviorEndpoint || '/recommendations/api/track-behavior/',
|
|
clickEndpoint: options.clickEndpoint || '/recommendations/api/track-recommendation-click/',
|
|
purchaseEndpoint: options.purchaseEndpoint || '/recommendations/api/track-recommendation-purchase/',
|
|
maxRecommendations: options.maxRecommendations || 6,
|
|
autoRefresh: options.autoRefresh || true,
|
|
refreshInterval: options.refreshInterval || 300000, // 5 minutes
|
|
showLoading: options.showLoading || true,
|
|
furryTheme: options.furryTheme || true,
|
|
...options
|
|
};
|
|
|
|
this.isAuthenticated = false;
|
|
this.currentRecommendations = [];
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.checkAuthentication();
|
|
this.setupEventListeners();
|
|
this.loadRecommendations();
|
|
|
|
if (this.options.autoRefresh) {
|
|
this.startAutoRefresh();
|
|
}
|
|
}
|
|
|
|
checkAuthentication() {
|
|
// Check if user is authenticated (CSRF token presence)
|
|
const csrfToken = this.getCookie('csrftoken');
|
|
this.isAuthenticated = !!csrfToken;
|
|
}
|
|
|
|
getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Track product views
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.closest('.product-card, .product-link')) {
|
|
const productId = e.target.closest('[data-product-id]')?.dataset.productId;
|
|
if (productId) {
|
|
this.trackBehavior('view', productId);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Track cart additions
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.closest('.add-to-cart')) {
|
|
const productId = e.target.closest('[data-product-id]')?.dataset.productId;
|
|
if (productId) {
|
|
this.trackBehavior('cart_add', productId);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Track wishlist additions
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.closest('.add-to-wishlist')) {
|
|
const productId = e.target.closest('[data-product-id]')?.dataset.productId;
|
|
if (productId) {
|
|
this.trackBehavior('wishlist_add', productId);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadRecommendations() {
|
|
if (!this.isAuthenticated) {
|
|
this.loadPopularProducts();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.options.showLoading) {
|
|
this.showLoading();
|
|
}
|
|
|
|
const response = await fetch(this.options.apiEndpoint, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCookie('csrftoken')
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.currentRecommendations = data.recommendations || [];
|
|
this.renderRecommendations();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading recommendations:', error);
|
|
this.loadPopularProducts();
|
|
}
|
|
}
|
|
|
|
async loadPopularProducts() {
|
|
try {
|
|
const response = await fetch('/recommendations/api/popular-products/');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.currentRecommendations = data.popular_products || [];
|
|
this.renderRecommendations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading popular products:', error);
|
|
}
|
|
}
|
|
|
|
renderRecommendations() {
|
|
const container = document.querySelector(this.options.container);
|
|
if (!container) return;
|
|
|
|
if (this.currentRecommendations.length === 0) {
|
|
container.innerHTML = this.renderEmptyState();
|
|
return;
|
|
}
|
|
|
|
const recommendationsHtml = this.currentRecommendations
|
|
.slice(0, this.options.maxRecommendations)
|
|
.map(rec => this.renderRecommendationCard(rec))
|
|
.join('');
|
|
|
|
container.innerHTML = `
|
|
<div class="recommendations-section">
|
|
<div class="recommendations-header">
|
|
<h3 class="recommendations-title">
|
|
${this.options.furryTheme ? '🦊 ' : ''}Empfohlen für dich
|
|
</h3>
|
|
<p class="recommendations-subtitle">Basierend auf deinen Präferenzen</p>
|
|
</div>
|
|
<div class="recommendations-grid">
|
|
${recommendationsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add click tracking
|
|
this.setupRecommendationTracking();
|
|
}
|
|
|
|
renderRecommendationCard(recommendation) {
|
|
const product = recommendation.product;
|
|
const confidencePercent = Math.round(recommendation.confidence_score * 100);
|
|
|
|
return `
|
|
<div class="recommendation-card" data-product-id="${product.id}" data-recommendation-type="${recommendation.recommendation_type}">
|
|
<div class="recommendation-badge">
|
|
${confidencePercent}% Match
|
|
</div>
|
|
<div class="recommendation-image">
|
|
<img src="${product.image_url || '/static/images/placeholder.jpg'}" alt="${product.name}">
|
|
</div>
|
|
<div class="recommendation-content">
|
|
<h4 class="recommendation-title">${product.name}</h4>
|
|
<p class="recommendation-price">€${product.price}</p>
|
|
<p class="recommendation-reason">${recommendation.reason}</p>
|
|
<div class="recommendation-actions">
|
|
<button class="btn btn-primary btn-sm add-to-cart" data-product-id="${product.id}">
|
|
Zum Warenkorb
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm add-to-wishlist" data-product-id="${product.id}">
|
|
<i class="fas fa-heart"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderEmptyState() {
|
|
return `
|
|
<div class="recommendations-empty">
|
|
<div class="empty-icon">${this.options.furryTheme ? '🦊' : '📦'}</div>
|
|
<h4>Noch keine Empfehlungen</h4>
|
|
<p>Interagiere mit Produkten, um personalisierte Empfehlungen zu erhalten!</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
setupRecommendationTracking() {
|
|
const cards = document.querySelectorAll('.recommendation-card');
|
|
cards.forEach(card => {
|
|
card.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.btn')) {
|
|
const productId = card.dataset.productId;
|
|
const recommendationType = card.dataset.recommendationType;
|
|
this.trackRecommendationClick(productId, recommendationType);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async trackBehavior(behaviorType, productId, metadata = {}) {
|
|
if (!this.isAuthenticated) return;
|
|
|
|
try {
|
|
await fetch(this.options.behaviorEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
behavior_type: behaviorType,
|
|
product_id: productId,
|
|
metadata: metadata
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Error tracking behavior:', error);
|
|
}
|
|
}
|
|
|
|
async trackRecommendationClick(productId, recommendationType) {
|
|
if (!this.isAuthenticated) return;
|
|
|
|
try {
|
|
await fetch(this.options.clickEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
product_id: productId,
|
|
recommendation_type: recommendationType
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Error tracking recommendation click:', error);
|
|
}
|
|
}
|
|
|
|
async trackRecommendationPurchase(productId, recommendationType) {
|
|
if (!this.isAuthenticated) return;
|
|
|
|
try {
|
|
await fetch(this.options.purchaseEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
product_id: productId,
|
|
recommendation_type: recommendationType
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Error tracking recommendation purchase:', error);
|
|
}
|
|
}
|
|
|
|
showLoading() {
|
|
const container = document.querySelector(this.options.container);
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="recommendations-loading">
|
|
<div class="loading-spinner"></div>
|
|
<p>Lade Empfehlungen...</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
startAutoRefresh() {
|
|
setInterval(() => {
|
|
this.loadRecommendations();
|
|
}, this.options.refreshInterval);
|
|
}
|
|
|
|
// Public methods
|
|
refresh() {
|
|
this.loadRecommendations();
|
|
}
|
|
|
|
updateOptions(newOptions) {
|
|
this.options = { ...this.options, ...newOptions };
|
|
}
|
|
}
|
|
|
|
// Similar Products Widget
|
|
class SimilarProductsWidget {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
container: options.container || '#similar-products',
|
|
apiEndpoint: options.apiEndpoint || '/recommendations/api/similar-products/',
|
|
maxProducts: options.maxProducts || 4,
|
|
...options
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.loadSimilarProducts();
|
|
}
|
|
|
|
async loadSimilarProducts(productId) {
|
|
if (!productId) return;
|
|
|
|
try {
|
|
const response = await fetch(`${this.options.apiEndpoint}${productId}/`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.renderSimilarProducts(data.similar_products);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading similar products:', error);
|
|
}
|
|
}
|
|
|
|
renderSimilarProducts(products) {
|
|
const container = document.querySelector(this.options.container);
|
|
if (!container || !products) return;
|
|
|
|
const productsHtml = products
|
|
.slice(0, this.options.maxProducts)
|
|
.map(product => this.renderProductCard(product))
|
|
.join('');
|
|
|
|
container.innerHTML = `
|
|
<div class="similar-products-section">
|
|
<h3 class="similar-products-title">Ähnliche Produkte</h3>
|
|
<div class="similar-products-grid">
|
|
${productsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderProductCard(product) {
|
|
return `
|
|
<div class="similar-product-card" data-product-id="${product.product.id}">
|
|
<div class="similarity-score">${Math.round(product.similarity_score * 100)}% ähnlich</div>
|
|
<div class="product-image">
|
|
<img src="${product.product.image_url || '/static/images/placeholder.jpg'}" alt="${product.product.name}">
|
|
</div>
|
|
<div class="product-content">
|
|
<h4 class="product-title">${product.product.name}</h4>
|
|
<p class="product-price">€${product.product.price}</p>
|
|
<p class="similarity-reason">${product.reason}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Initialize widgets when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize recommendation widget
|
|
if (document.querySelector('#recommendations')) {
|
|
window.recommendationWidget = new RecommendationWidget({
|
|
container: '#recommendations',
|
|
maxRecommendations: 6,
|
|
furryTheme: true
|
|
});
|
|
}
|
|
|
|
// Initialize similar products widget
|
|
if (document.querySelector('#similar-products')) {
|
|
window.similarProductsWidget = new SimilarProductsWidget({
|
|
container: '#similar-products',
|
|
maxProducts: 4
|
|
});
|
|
|
|
// Load similar products for current product
|
|
const productId = document.querySelector('[data-product-id]')?.dataset.productId;
|
|
if (productId) {
|
|
window.similarProductsWidget.loadSimilarProducts(productId);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Export for global access
|
|
window.RecommendationWidget = RecommendationWidget;
|
|
window.SimilarProductsWidget = SimilarProductsWidget;
|