furry/static/js/chat-widget.js

922 lines
29 KiB
JavaScript

/**
* Kasico Chat Widget - Furry-Style Live-Chat
* WebSocket-basiertes Chat-System
*/
class KasicoChatWidget {
constructor() {
this.isOpen = false;
this.isConnected = false;
this.currentRoom = null;
this.websocket = null;
this.notificationSocket = null;
this.typingTimeout = null;
this.unreadCount = 0;
this.init();
}
init() {
// Chat-Widget HTML erstellen
this.createWidget();
// Event Listeners
this.bindEvents();
// WebSocket-Verbindung
this.connectWebSocket();
// Auto-Show nach 30 Sekunden
setTimeout(() => {
if (!this.isOpen && !this.hasInteracted()) {
this.showWelcomeMessage();
}
}, 30000);
}
createWidget() {
const widgetHTML = `
<div id="kasico-chat-widget" class="chat-widget">
<!-- Chat Button -->
<div class="chat-button" id="chat-button">
<div class="chat-button-icon">
<i class="fas fa-comments"></i>
<span class="chat-notification-badge" id="chat-notification-badge" style="display: none;">0</span>
</div>
<div class="chat-button-text">
<span class="chat-button-title">Chat</span>
<span class="chat-button-subtitle">Online</span>
</div>
</div>
<!-- Chat Window -->
<div class="chat-window" id="chat-window">
<!-- Chat Header -->
<div class="chat-header">
<div class="chat-header-info">
<div class="chat-avatar">
<img src="/static/images/dog-user-icon.svg" alt="Kasico Support">
</div>
<div class="chat-header-text">
<h4>Kasico Support</h4>
<span class="chat-status online">Online</span>
</div>
</div>
<div class="chat-header-actions">
<button class="chat-minimize" id="chat-minimize">
<i class="fas fa-minus"></i>
</button>
<button class="chat-close" id="chat-close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Chat Messages -->
<div class="chat-messages" id="chat-messages">
<div class="chat-welcome">
<div class="chat-welcome-icon">
<i class="fas fa-paw"></i>
</div>
<h5>Willkommen bei Kasico! 🐾</h5>
<p>Wie können wir dir heute helfen?</p>
<div class="chat-quick-actions">
<button class="quick-action" data-action="pricing">
<i class="fas fa-tag"></i> Preise
</button>
<button class="quick-action" data-action="custom-order">
<i class="fas fa-magic"></i> Custom Order
</button>
<button class="quick-action" data-action="shipping">
<i class="fas fa-shipping-fast"></i> Versand
</button>
</div>
</div>
</div>
<!-- Chat Input -->
<div class="chat-input-container">
<div class="chat-input-wrapper">
<textarea
class="chat-input"
id="chat-input"
placeholder="Schreibe eine Nachricht..."
rows="1"
></textarea>
<div class="chat-input-actions">
<button class="chat-attachment" id="chat-attachment">
<i class="fas fa-paperclip"></i>
</button>
<button class="chat-send" id="chat-send">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<div class="chat-typing" id="chat-typing" style="display: none;">
<span class="typing-indicator">
<span></span>
<span></span>
<span></span>
</span>
<span class="typing-text">Support schreibt...</span>
</div>
</div>
</div>
<!-- File Upload Input -->
<input type="file" id="chat-file-input" accept="image/*,.pdf,.doc,.docx" style="display: none;">
</div>
`;
document.body.insertAdjacentHTML('beforeend', widgetHTML);
// CSS hinzufügen
this.addStyles();
}
addStyles() {
const styles = `
.chat-widget {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
font-family: 'Quicksand', sans-serif;
}
.chat-button {
background: linear-gradient(135deg, #FF6B9D, #FF8E53);
color: white;
border: none;
border-radius: 25px;
padding: 15px 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 20px rgba(255, 107, 157, 0.3);
transition: all 0.3s ease;
min-width: 200px;
}
.chat-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(255, 107, 157, 0.4);
}
.chat-button-icon {
position: relative;
font-size: 20px;
}
.chat-notification-badge {
position: absolute;
top: -8px;
right: -8px;
background: #FF4757;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.chat-button-text {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.chat-button-title {
font-weight: 600;
font-size: 16px;
}
.chat-button-subtitle {
font-size: 12px;
opacity: 0.9;
}
.chat-window {
position: absolute;
bottom: 80px;
right: 0;
width: 350px;
height: 500px;
background: white;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
overflow: hidden;
border: 2px solid #FF6B9D;
}
.chat-header {
background: linear-gradient(135deg, #FF6B9D, #FF8E53);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header-info {
display: flex;
align-items: center;
gap: 12px;
}
.chat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
border: 2px solid white;
}
.chat-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.chat-header-text h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.chat-status {
font-size: 12px;
opacity: 0.9;
}
.chat-status.online::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background: #2ED573;
border-radius: 50%;
margin-right: 5px;
}
.chat-header-actions {
display: flex;
gap: 8px;
}
.chat-header-actions button {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
}
.chat-header-actions button:hover {
background: rgba(255, 255, 255, 0.3);
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #F8F9FA;
}
.chat-welcome {
text-align: center;
padding: 20px;
background: white;
border-radius: 15px;
margin-bottom: 20px;
}
.chat-welcome-icon {
font-size: 40px;
color: #FF6B9D;
margin-bottom: 15px;
}
.chat-welcome h5 {
margin: 0 0 10px 0;
color: #333;
}
.chat-welcome p {
margin: 0 0 20px 0;
color: #666;
}
.chat-quick-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.quick-action {
background: linear-gradient(135deg, #FF6B9D, #FF8E53);
color: white;
border: none;
border-radius: 20px;
padding: 10px 15px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.quick-action:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(255, 107, 157, 0.3);
}
.chat-message {
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.chat-message.sent {
align-items: flex-end;
}
.chat-message.received {
align-items: flex-start;
}
.chat-message.system {
align-items: center;
}
.message-bubble {
max-width: 80%;
padding: 12px 16px;
border-radius: 20px;
font-size: 14px;
line-height: 1.4;
position: relative;
}
.message-bubble.sent {
background: linear-gradient(135deg, #FF6B9D, #FF8E53);
color: white;
border-bottom-right-radius: 5px;
}
.message-bubble.received {
background: white;
color: #333;
border: 1px solid #E9ECEF;
border-bottom-left-radius: 5px;
}
.message-bubble.system {
background: #F8F9FA;
color: #666;
font-size: 12px;
font-style: italic;
border-radius: 15px;
}
.message-time {
font-size: 11px;
opacity: 0.7;
margin-top: 5px;
}
.message-time.sent {
text-align: right;
}
.message-time.received {
text-align: left;
}
.message-time.system {
text-align: center;
}
.chat-input-container {
padding: 15px 20px;
background: white;
border-top: 1px solid #E9ECEF;
}
.chat-input-wrapper {
display: flex;
align-items: flex-end;
gap: 10px;
}
.chat-input {
flex: 1;
border: 1px solid #E9ECEF;
border-radius: 20px;
padding: 12px 16px;
font-size: 14px;
resize: none;
outline: none;
transition: border-color 0.3s ease;
font-family: inherit;
}
.chat-input:focus {
border-color: #FF6B9D;
}
.chat-input-actions {
display: flex;
gap: 8px;
}
.chat-input-actions button {
background: linear-gradient(135deg, #FF6B9D, #FF8E53);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.chat-input-actions button:hover {
transform: scale(1.1);
}
.chat-input-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.chat-typing {
padding: 10px 20px;
display: flex;
align-items: center;
gap: 10px;
color: #666;
font-size: 12px;
}
.typing-indicator {
display: flex;
gap: 3px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: #FF6B9D;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
.chat-widget.hidden .chat-window {
display: none;
}
.chat-widget.open .chat-window {
display: flex;
}
/* Responsive Design */
@media (max-width: 768px) {
.chat-widget {
bottom: 10px;
right: 10px;
}
.chat-window {
width: calc(100vw - 20px);
height: calc(100vh - 100px);
bottom: 70px;
}
.chat-button {
min-width: auto;
padding: 12px 15px;
}
.chat-button-text {
display: none;
}
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
bindEvents() {
// Chat Button
document.getElementById('chat-button').addEventListener('click', () => {
this.toggleChat();
});
// Chat Controls
document.getElementById('chat-minimize').addEventListener('click', () => {
this.toggleChat();
});
document.getElementById('chat-close').addEventListener('click', () => {
this.closeChat();
});
// Chat Input
const chatInput = document.getElementById('chat-input');
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
chatInput.addEventListener('input', () => {
this.adjustTextareaHeight(chatInput);
this.sendTypingIndicator(true);
});
// Send Button
document.getElementById('chat-send').addEventListener('click', () => {
this.sendMessage();
});
// Attachment Button
document.getElementById('chat-attachment').addEventListener('click', () => {
document.getElementById('chat-file-input').click();
});
// File Upload
document.getElementById('chat-file-input').addEventListener('change', (e) => {
this.handleFileUpload(e.target.files[0]);
});
// Quick Actions
document.querySelectorAll('.quick-action').forEach(button => {
button.addEventListener('click', (e) => {
const action = e.target.closest('.quick-action').dataset.action;
this.handleQuickAction(action);
});
});
}
connectWebSocket() {
// Chat WebSocket
this.connectChatWebSocket();
// Notification WebSocket
this.connectNotificationWebSocket();
}
connectChatWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/chat/${this.currentRoom || 'new'}/`;
this.websocket = new WebSocket(wsUrl);
this.websocket.onopen = () => {
console.log('Chat 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('Chat WebSocket disconnected');
this.isConnected = false;
this.updateConnectionStatus(false);
// Reconnect nach 5 Sekunden
setTimeout(() => {
if (!this.isConnected) {
this.connectChatWebSocket();
}
}, 5000);
};
this.websocket.onerror = (error) => {
console.error('Chat WebSocket error:', error);
};
}
connectNotificationWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/notifications/`;
this.notificationSocket = new WebSocket(wsUrl);
this.notificationSocket.onopen = () => {
console.log('Notification WebSocket connected');
};
this.notificationSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleNotificationMessage(data);
};
}
handleWebSocketMessage(data) {
switch (data.type) {
case 'message':
this.addMessage(data.message);
break;
case 'typing':
this.showTypingIndicator(data.user, data.typing);
break;
case 'read':
this.markMessagesAsRead(data.user_id);
break;
case 'system':
this.addSystemMessage(data.message);
break;
case 'error':
this.showError(data.message);
break;
}
}
handleNotificationMessage(data) {
if (data.type === 'notification') {
this.showNotification(data.notification);
}
}
sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message || !this.isConnected) return;
// Nachricht senden
this.websocket.send(JSON.stringify({
type: 'message',
content: message,
message_type: 'text'
}));
// Input leeren
input.value = '';
this.adjustTextareaHeight(input);
// Typing Indicator stoppen
this.sendTypingIndicator(false);
}
sendTypingIndicator(typing) {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}
if (typing) {
this.websocket.send(JSON.stringify({
type: 'typing',
typing: true
}));
this.typingTimeout = setTimeout(() => {
this.sendTypingIndicator(false);
}, 3000);
} else {
this.websocket.send(JSON.stringify({
type: 'typing',
typing: false
}));
}
}
addMessage(message) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
const isOwnMessage = message.sender_id === this.getCurrentUserId();
const messageClass = isOwnMessage ? 'sent' : 'received';
messageDiv.className = `chat-message ${messageClass}`;
let messageContent = '';
if (message.message_type === 'image') {
messageContent = `<img src="${message.image_url}" alt="Bild" style="max-width: 200px; border-radius: 10px;">`;
} else if (message.message_type === 'file') {
messageContent = `<div class="file-message">
<i class="fas fa-file"></i>
<span>${message.content}</span>
</div>`;
} else {
messageContent = message.content;
}
messageDiv.innerHTML = `
<div class="message-bubble ${messageClass}">
${messageContent}
</div>
<div class="message-time ${messageClass}">
${this.formatTime(message.created_at)}
</div>
`;
messagesContainer.appendChild(messageDiv);
this.scrollToBottom();
// Notification Badge aktualisieren
if (!isOwnMessage && !this.isOpen) {
this.incrementUnreadCount();
}
}
addSystemMessage(message) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'chat-message system';
messageDiv.innerHTML = `
<div class="message-bubble system">
${message}
</div>
<div class="message-time system">
${this.formatTime(new Date())}
</div>
`;
messagesContainer.appendChild(messageDiv);
this.scrollToBottom();
}
showTypingIndicator(user, typing) {
const typingElement = document.getElementById('chat-typing');
if (typing) {
typingElement.style.display = 'flex';
} else {
typingElement.style.display = 'none';
}
}
showWelcomeMessage() {
this.addSystemMessage('Willkommen! Wie können wir dir helfen? 🐾');
}
handleQuickAction(action) {
const messages = {
'pricing': 'Hier findest du unsere Preise: https://kasico.de/pricing',
'custom-order': 'Für Custom Orders: https://kasico.de/custom-order',
'shipping': 'Versandinfo: https://kasico.de/shipping'
};
this.addMessage({
sender_id: this.getCurrentUserId(),
content: messages[action],
message_type: 'text',
created_at: new Date().toISOString()
});
// Nachricht senden
this.websocket.send(JSON.stringify({
type: 'message',
content: messages[action],
message_type: 'text'
}));
}
handleFileUpload(file) {
if (!file) return;
// Datei-Upload simulieren (in Produktion würde hier ein echter Upload stattfinden)
const fileUrl = URL.createObjectURL(file);
this.websocket.send(JSON.stringify({
type: 'file_upload',
file_url: fileUrl,
filename: file.name,
file_type: file.type.startsWith('image/') ? 'image' : 'file'
}));
}
toggleChat() {
const widget = document.getElementById('kasico-chat-widget');
if (this.isOpen) {
widget.classList.remove('open');
this.isOpen = false;
} else {
widget.classList.add('open');
this.isOpen = true;
this.clearUnreadCount();
this.scrollToBottom();
}
}
closeChat() {
const widget = document.getElementById('kasico-chat-widget');
widget.classList.remove('open');
this.isOpen = false;
}
updateConnectionStatus(connected) {
const statusElement = document.querySelector('.chat-status');
const button = document.getElementById('chat-button');
if (connected) {
statusElement.textContent = 'Online';
statusElement.className = 'chat-status online';
button.style.opacity = '1';
} else {
statusElement.textContent = 'Offline';
statusElement.className = 'chat-status offline';
button.style.opacity = '0.7';
}
}
incrementUnreadCount() {
this.unreadCount++;
const badge = document.getElementById('chat-notification-badge');
badge.textContent = this.unreadCount;
badge.style.display = 'flex';
}
clearUnreadCount() {
this.unreadCount = 0;
const badge = document.getElementById('chat-notification-badge');
badge.style.display = 'none';
}
scrollToBottom() {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
adjustTextareaHeight(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 100) + 'px';
}
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
}
getCurrentUserId() {
// In einer echten Implementierung würde hier die User-ID aus dem Session/Token geholt
return 1; // Placeholder
}
hasInteracted() {
// Prüfen ob User bereits mit der Seite interagiert hat
return sessionStorage.getItem('chat_interaction') === 'true';
}
showError(message) {
this.addSystemMessage(`Fehler: ${message}`);
}
showNotification(notification) {
// Browser Notification anzeigen
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: '/static/images/kasico-logo.png'
});
}
}
}
// Chat Widget initialisieren
document.addEventListener('DOMContentLoaded', function() {
window.kasicoChat = new KasicoChatWidget();
// Notification Permission anfordern
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
});