furry/static/js/auction-widget.js

870 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Kasico Auction Widget - Real-time Biet-System
* WebSocket-basierte Live-Auktionen mit Furry-Design
*/
class KasicoAuctionWidget {
constructor(auctionId) {
this.auctionId = auctionId;
this.websocket = null;
this.currentBid = null;
this.timeRemaining = null;
this.isConnected = false;
this.bidHistory = [];
this.watchers = [];
this.init();
}
init() {
// Auction Widget HTML erstellen
this.createWidget();
// Event Listeners
this.bindEvents();
// WebSocket-Verbindung
this.connectWebSocket();
// Initial data laden
this.loadAuctionData();
// Timer starten
this.startTimer();
}
createWidget() {
const widgetHTML = `
<div id="kasico-auction-widget" class="auction-widget">
<!-- Auction Header -->
<div class="auction-header">
<div class="auction-title">
<h2 id="auction-title">Lade Auktion...</h2>
<div class="auction-status" id="auction-status">
<span class="status-indicator"></span>
<span class="status-text">Lade...</span>
</div>
</div>
<div class="auction-timer" id="auction-timer">
<i class="fas fa-clock"></i>
<span class="timer-text">--:--:--</span>
</div>
</div>
<!-- Current Bid -->
<div class="current-bid-section">
<div class="current-bid-display">
<div class="bid-label">Aktuelles Gebot</div>
<div class="bid-amount" id="current-bid-amount">€0.00</div>
<div class="bid-info">
<span class="bidder" id="current-bidder">Keine Gebote</span>
<span class="bid-time" id="bid-time">-</span>
</div>
</div>
<!-- Bid Form -->
<div class="bid-form" id="bid-form">
<div class="bid-input-group">
<span class="currency-symbol">€</span>
<input type="number"
id="bid-amount"
class="bid-input"
placeholder="Dein Gebot"
min="0"
step="0.01">
</div>
<button class="bid-button" id="place-bid-btn">
<i class="fas fa-gavel"></i>
Gebot abgeben
</button>
</div>
<!-- Reserve Price Info -->
<div class="reserve-info" id="reserve-info" style="display: none;">
<i class="fas fa-shield-alt"></i>
<span>Reserve Price: €<span id="reserve-price">0.00</span></span>
</div>
</div>
<!-- Bid History -->
<div class="bid-history-section">
<div class="section-header">
<h3><i class="fas fa-history"></i> Gebot-Historie</h3>
<span class="bid-count" id="bid-count">0 Gebote</span>
</div>
<div class="bid-history" id="bid-history">
<div class="empty-history">
<i class="fas fa-gavel"></i>
<p>Noch keine Gebote</p>
</div>
</div>
</div>
<!-- Auction Info -->
<div class="auction-info-section">
<div class="info-grid">
<div class="info-item">
<i class="fas fa-users"></i>
<div class="info-content">
<span class="info-label">Bieter</span>
<span class="info-value" id="total-bidders">0</span>
</div>
</div>
<div class="info-item">
<i class="fas fa-eye"></i>
<div class="info-content">
<span class="info-label">Aufrufe</span>
<span class="info-value" id="view-count">0</span>
</div>
</div>
<div class="info-item">
<i class="fas fa-heart"></i>
<div class="info-content">
<span class="info-label">Beobachter</span>
<span class="info-value" id="watcher-count">0</span>
</div>
</div>
</div>
</div>
<!-- Watch Button -->
<div class="watch-section">
<button class="watch-button" id="watch-button">
<i class="fas fa-heart"></i>
<span class="watch-text">Beobachten</span>
</button>
</div>
</div>
`;
// Widget in die Seite einfügen
const container = document.getElementById('auction-container') || document.body;
container.insertAdjacentHTML('beforeend', widgetHTML);
// CSS hinzufügen
this.addStyles();
}
addStyles() {
const styles = `
.auction-widget {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
padding: 25px;
color: white;
font-family: 'Quicksand', sans-serif;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
margin: 20px 0;
}
.auction-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);
}
.auction-title h2 {
margin: 0 0 5px 0;
font-size: 24px;
font-weight: 700;
}
.auction-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #4CAF50;
animation: pulse 2s infinite;
}
.status-indicator.ended {
background: #f44336;
animation: none;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.auction-timer {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
padding: 12px 20px;
border-radius: 25px;
font-weight: 600;
}
.timer-text {
font-size: 18px;
font-family: 'Courier New', monospace;
}
.current-bid-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
}
.current-bid-display {
text-align: center;
margin-bottom: 20px;
}
.bid-label {
font-size: 14px;
opacity: 0.8;
margin-bottom: 5px;
}
.bid-amount {
font-size: 36px;
font-weight: 700;
margin-bottom: 10px;
color: #FFD700;
}
.bid-info {
display: flex;
justify-content: center;
gap: 20px;
font-size: 14px;
opacity: 0.8;
}
.bid-form {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 15px;
}
.bid-input-group {
position: relative;
flex: 1;
}
.currency-symbol {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
font-weight: 600;
color: #333;
}
.bid-input {
width: 100%;
padding: 15px 15px 15px 35px;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
background: white;
color: #333;
outline: none;
transition: all 0.3s ease;
}
.bid-input:focus {
box-shadow: 0 0 0 3px rgba(255, 215, 0, 0.3);
}
.bid-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;
}
.bid-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
}
.bid-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.reserve-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
opacity: 0.8;
justify-content: center;
}
.bid-history-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-bottom: 25px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-header h3 {
margin: 0;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.bid-count {
font-size: 14px;
opacity: 0.8;
}
.bid-history {
max-height: 200px;
overflow-y: auto;
}
.bid-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.bid-item:last-child {
border-bottom: none;
}
.bid-user {
font-weight: 600;
}
.bid-amount-small {
color: #FFD700;
font-weight: 600;
}
.bid-time-small {
font-size: 12px;
opacity: 0.7;
}
.empty-history {
text-align: center;
padding: 30px;
opacity: 0.7;
}
.empty-history i {
font-size: 24px;
margin-bottom: 10px;
}
.auction-info-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-bottom: 25px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.info-item {
display: flex;
align-items: center;
gap: 12px;
}
.info-item i {
font-size: 20px;
color: #FFD700;
}
.info-content {
display: flex;
flex-direction: column;
}
.info-label {
font-size: 12px;
opacity: 0.8;
}
.info-value {
font-size: 18px;
font-weight: 600;
}
.watch-section {
text-align: center;
}
.watch-button {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 25px;
padding: 12px 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.watch-button:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}
.watch-button.watching {
background: #e91e63;
border-color: #e91e63;
}
.watch-button.watching:hover {
background: #c2185b;
border-color: #c2185b;
}
/* Responsive Design */
@media (max-width: 768px) {
.auction-widget {
padding: 20px;
}
.auction-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.bid-form {
flex-direction: column;
}
.info-grid {
grid-template-columns: 1fr;
gap: 15px;
}
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
bindEvents() {
// Bid Button
document.getElementById('place-bid-btn').addEventListener('click', () => {
this.placeBid();
});
// Bid Input
document.getElementById('bid-amount').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.placeBid();
}
});
// Watch Button
document.getElementById('watch-button').addEventListener('click', () => {
this.toggleWatch();
});
}
connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/auction/${this.auctionId}/`;
this.websocket = new WebSocket(wsUrl);
this.websocket.onopen = () => {
console.log('Auction WebSocket connected');
this.isConnected = true;
this.updateConnectionStatus(true);
};
this.websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
};
this.websocket.onclose = () => {
console.log('Auction WebSocket disconnected');
this.isConnected = false;
this.updateConnectionStatus(false);
// Reconnect nach 5 Sekunden
setTimeout(() => {
if (!this.isConnected) {
this.connectWebSocket();
}
}, 5000);
};
this.websocket.onerror = (error) => {
console.error('Auction WebSocket error:', error);
};
}
handleWebSocketMessage(data) {
switch (data.type) {
case 'bid_placed':
this.handleNewBid(data.bid);
break;
case 'auction_updated':
this.updateAuctionData(data.auction);
break;
case 'time_update':
this.updateTimer(data.time_remaining);
break;
case 'auction_ended':
this.handleAuctionEnded(data.winner);
break;
}
}
async loadAuctionData() {
try {
const response = await fetch(`/auction/api/${this.auctionId}/`);
const data = await response.json();
this.updateAuctionData(data);
} catch (error) {
console.error('Error loading auction data:', error);
}
}
updateAuctionData(data) {
// Basic info
document.getElementById('auction-title').textContent = data.title;
document.getElementById('current-bid-amount').textContent =
data.current_bid ? `${parseFloat(data.current_bid).toFixed(2)}` : `${parseFloat(data.starting_bid).toFixed(2)}`;
// Status
const statusElement = document.getElementById('auction-status');
const statusText = statusElement.querySelector('.status-text');
const statusIndicator = statusElement.querySelector('.status-indicator');
statusText.textContent = data.is_active ? 'Aktiv' : 'Beendet';
statusIndicator.className = `status-indicator ${data.is_active ? '' : 'ended'}`;
// Reserve price
if (data.reserve_price) {
document.getElementById('reserve-price').textContent = parseFloat(data.reserve_price).toFixed(2);
document.getElementById('reserve-info').style.display = 'flex';
}
// Stats
document.getElementById('total-bidders').textContent = data.total_bidders;
document.getElementById('bid-count').textContent = `${data.total_bids} Gebote`;
// Store current data
this.currentBid = data.current_bid;
this.timeRemaining = data.time_remaining;
// Update timer
this.updateTimer(data.time_remaining);
// Load bid history
this.loadBidHistory();
}
async loadBidHistory() {
try {
const response = await fetch(`/auction/api/${this.auctionId}/bids/`);
const data = await response.json();
this.updateBidHistory(data.bids);
} catch (error) {
console.error('Error loading bid history:', error);
}
}
updateBidHistory(bids) {
const historyContainer = document.getElementById('bid-history');
if (bids.length === 0) {
historyContainer.innerHTML = `
<div class="empty-history">
<i class="fas fa-gavel"></i>
<p>Noch keine Gebote</p>
</div>
`;
return;
}
historyContainer.innerHTML = bids.map(bid => `
<div class="bid-item">
<div class="bid-user">${bid.bidder}</div>
<div class="bid-amount-small">€${parseFloat(bid.amount).toFixed(2)}</div>
<div class="bid-time-small">${this.formatTime(bid.created_at)}</div>
</div>
`).join('');
}
handleNewBid(bid) {
// Update current bid
document.getElementById('current-bid-amount').textContent = `${parseFloat(bid.amount).toFixed(2)}`;
document.getElementById('current-bidder').textContent = bid.bidder;
document.getElementById('bid-time').textContent = this.formatTime(bid.created_at);
// Add to history
const historyContainer = document.getElementById('bid-history');
const bidItem = document.createElement('div');
bidItem.className = 'bid-item';
bidItem.innerHTML = `
<div class="bid-user">${bid.bidder}</div>
<div class="bid-amount-small">€${parseFloat(bid.amount).toFixed(2)}</div>
<div class="bid-time-small">${this.formatTime(bid.created_at)}</div>
`;
historyContainer.insertBefore(bidItem, historyContainer.firstChild);
// Update stats
const bidCount = document.getElementById('bid-count');
const currentCount = parseInt(bidCount.textContent.match(/\d+/)[0]);
bidCount.textContent = `${currentCount + 1} Gebote`;
// Animation
bidItem.style.animation = 'slideIn 0.3s ease-out';
// Show notification
this.showNotification(`Neues Gebot: €${parseFloat(bid.amount).toFixed(2)} von ${bid.bidder}`);
}
async placeBid() {
const input = document.getElementById('bid-amount');
const amount = parseFloat(input.value);
if (!amount || amount <= 0) {
this.showError('Bitte gib ein gültiges Gebot ein');
return;
}
if (this.currentBid && amount <= this.currentBid) {
this.showError('Gebot muss höher als das aktuelle Gebot sein');
return;
}
const button = document.getElementById('place-bid-btn');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sende...';
try {
const response = await fetch(`/auction/api/${this.auctionId}/bid/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCSRFToken(),
},
body: JSON.stringify({ amount: amount })
});
const data = await response.json();
if (data.success) {
input.value = '';
this.showSuccess('Gebot erfolgreich platziert!');
// Update via WebSocket
this.websocket.send(JSON.stringify({
type: 'bid_placed',
amount: amount
}));
} else {
this.showError(data.error || 'Fehler beim Platzieren des Gebots');
}
} catch (error) {
this.showError('Netzwerkfehler');
} finally {
button.disabled = false;
button.innerHTML = originalText;
}
}
async toggleWatch() {
const button = document.getElementById('watch-button');
const originalText = button.querySelector('.watch-text').textContent;
button.disabled = true;
button.querySelector('.watch-text').textContent = '...';
try {
const response = await fetch(`/auction/${this.auctionId}/watch/`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
}
});
const data = await response.json();
if (data.success) {
if (data.is_watching) {
button.classList.add('watching');
button.querySelector('.watch-text').textContent = 'Beobachtet';
} else {
button.classList.remove('watching');
button.querySelector('.watch-text').textContent = 'Beobachten';
}
this.showSuccess(data.message);
}
} catch (error) {
this.showError('Fehler beim Aktualisieren der Watchlist');
} finally {
button.disabled = false;
}
}
updateTimer(timeRemaining) {
const timerElement = document.getElementById('auction-timer');
const timerText = timerElement.querySelector('.timer-text');
if (!timeRemaining) {
timerText.textContent = 'Beendet';
return;
}
timerText.textContent = timeRemaining;
}
handleAuctionEnded(winner) {
const statusElement = document.getElementById('auction-status');
const statusText = statusElement.querySelector('.status-text');
const statusIndicator = statusElement.querySelector('.status-indicator');
statusText.textContent = 'Beendet';
statusIndicator.className = 'status-indicator ended';
// Disable bid form
document.getElementById('bid-form').style.display = 'none';
// Show winner
if (winner) {
this.showNotification(`Auktion beendet! Gewinner: ${winner}`);
} else {
this.showNotification('Auktion beendet - Kein Gewinner');
}
}
startTimer() {
setInterval(() => {
if (this.timeRemaining) {
// Timer logic would be implemented here
// For now, just update every second
}
}, 1000);
}
updateConnectionStatus(connected) {
const statusElement = document.getElementById('auction-status');
const statusIndicator = statusElement.querySelector('.status-indicator');
if (connected) {
statusIndicator.style.background = '#4CAF50';
} else {
statusIndicator.style.background = '#f44336';
}
}
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
}
getCSRFToken() {
return document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showError(message) {
this.showNotification(message, 'error');
}
showNotification(message, type = 'info') {
// Browser notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('Kasico Auction', {
body: message,
icon: '/static/images/kasico-logo.png'
});
}
// Toast notification
const toast = document.createElement('div');
toast.className = `toast-notification ${type}`;
toast.innerHTML = `
<div class="toast-header">
<strong>${type === 'success' ? '✅' : type === 'error' ? '❌' : ''} ${message}</strong>
<button type="button" class="toast-close">&times;</button>
</div>
`;
document.body.appendChild(toast);
// Auto remove after 5 seconds
setTimeout(() => {
toast.remove();
}, 5000);
// Close button
toast.querySelector('.toast-close').addEventListener('click', () => {
toast.remove();
});
}
}
// Auction Widget initialisieren
document.addEventListener('DOMContentLoaded', function() {
const auctionId = document.querySelector('[data-auction-id]')?.dataset.auctionId;
if (auctionId) {
window.kasicoAuction = new KasicoAuctionWidget(auctionId);
}
});