feat: Complete chat system implementation and password reset enhancement
- Add comprehensive chat system with modern UI design - Implement admin-based password reset system - Fix template syntax errors and 500 server errors - Add chat routes, API endpoints, and database models - Enhance user interface with Tailwind CSS card-based design - Implement community guidelines and quick action features - Add responsive design for mobile and desktop compatibility - Create support chat functionality with admin integration - Fix JavaScript inheritance in base template - Add database migration for chat system tables Features: ✅ Modern chat interface with room management ✅ Admin-based password reset workflow ✅ Real-time chat with mobile app support ✅ Professional UI with gradient cards and hover effects ✅ Community guidelines and safety features ✅ Responsive design for all devices ✅ Error-free template rendering
This commit is contained in:
313
app/templates/chat/embed.html
Normal file
313
app/templates/chat/embed.html
Normal file
@@ -0,0 +1,313 @@
|
||||
<!-- Embeddable Chat Widget for Posts and Pages -->
|
||||
<div class="chat-embed-widget" data-post-id="{{ post.id if post else '' }}" style="margin: 1rem 0;">
|
||||
<div class="chat-embed-header" onclick="toggleChatEmbed(this)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-comments me-2"></i>
|
||||
<span class="chat-embed-title">
|
||||
{% if post %}
|
||||
Discussion: {{ post.title[:50] }}{% if post.title|length > 50 %}...{% endif %}
|
||||
{% else %}
|
||||
Join the Discussion
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge bg-primary ms-2" id="messageCount-{{ post.id if post else 'general' }}">
|
||||
{{ message_count or 0 }} messages
|
||||
</span>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down chat-embed-toggle"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-embed-content" style="display: none;">
|
||||
<div class="chat-embed-messages" id="embedMessages-{{ post.id if post else 'general' }}">
|
||||
{% if recent_messages %}
|
||||
{% for message in recent_messages %}
|
||||
<div class="chat-embed-message">
|
||||
<div class="message-header">
|
||||
<strong>{{ message.user.nickname }}</strong>
|
||||
{% if message.user.is_admin %}
|
||||
<span class="badge bg-danger ms-1">ADMIN</span>
|
||||
{% endif %}
|
||||
<small class="text-muted ms-2">{{ message.created_at.strftime('%H:%M') }}</small>
|
||||
</div>
|
||||
<div class="message-content">{{ message.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if message_count > recent_messages|length %}
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">+ {{ message_count - recent_messages|length }} more messages</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-comments fa-2x mb-2"></i>
|
||||
<p>No messages yet. Start the conversation!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="chat-embed-input">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
placeholder="Type your message..."
|
||||
id="embedInput-{{ post.id if post else 'general' }}"
|
||||
maxlength="500"
|
||||
onkeypress="handleEmbedEnter(event, '{{ post.id if post else 'general' }}')">
|
||||
<button class="btn btn-primary"
|
||||
type="button"
|
||||
onclick="sendEmbedMessage('{{ post.id if post else 'general' }}')">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Max 500 characters</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="chat-embed-login text-center py-3">
|
||||
<p class="text-muted mb-2">Join the discussion</p>
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-primary btn-sm me-2">Login</a>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-outline-primary btn-sm">Register</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="chat-embed-actions text-center mt-2">
|
||||
{% if room_id %}
|
||||
<a href="{{ url_for('chat.room', room_id=room_id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-expand-alt me-1"></i>
|
||||
Open Full Chat
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('chat.index') }}" class="btn btn-sm btn-outline-secondary ms-2">
|
||||
<i class="fas fa-comments me-1"></i>
|
||||
All Chats
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chat-embed-widget {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-embed-widget:hover {
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.chat-embed-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-embed-header:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
|
||||
.chat-embed-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-embed-toggle {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-embed-widget.expanded .chat-embed-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.chat-embed-content {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.chat-embed-messages {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chat-embed-message {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.chat-embed-message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-embed-message .message-header {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.chat-embed-message .message-content {
|
||||
color: #495057;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chat-embed-input {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.chat-embed-login {
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.chat-embed-actions {
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-embed-widget {
|
||||
margin: 1rem -15px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-embed-messages {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleChatEmbed(header) {
|
||||
const widget = header.closest('.chat-embed-widget');
|
||||
const content = widget.querySelector('.chat-embed-content');
|
||||
const isExpanded = widget.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
content.style.display = 'none';
|
||||
widget.classList.remove('expanded');
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
widget.classList.add('expanded');
|
||||
|
||||
// Load recent messages if not already loaded
|
||||
const postId = widget.dataset.postId;
|
||||
if (postId) {
|
||||
loadEmbedMessages(postId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadEmbedMessages(postId) {
|
||||
const messagesContainer = document.getElementById(`embedMessages-${postId}`);
|
||||
|
||||
fetch(`/api/v1/chat/embed/messages?post_id=${postId}&limit=5`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.messages.length > 0) {
|
||||
let messagesHTML = '';
|
||||
data.messages.forEach(message => {
|
||||
messagesHTML += `
|
||||
<div class="chat-embed-message">
|
||||
<div class="message-header">
|
||||
<strong>${message.user.nickname}</strong>
|
||||
${message.user.is_admin ? '<span class="badge bg-danger ms-1">ADMIN</span>' : ''}
|
||||
<small class="text-muted ms-2">${new Date(message.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</small>
|
||||
</div>
|
||||
<div class="message-content">${message.content}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (data.total_count > data.messages.length) {
|
||||
messagesHTML += `
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">+ ${data.total_count - data.messages.length} more messages</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
messagesContainer.innerHTML = messagesHTML;
|
||||
|
||||
// Update message count
|
||||
const countBadge = document.getElementById(`messageCount-${postId}`);
|
||||
if (countBadge) {
|
||||
countBadge.textContent = `${data.total_count} messages`;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading embed messages:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function handleEmbedEnter(event, postId) {
|
||||
if (event.key === 'Enter') {
|
||||
sendEmbedMessage(postId);
|
||||
}
|
||||
}
|
||||
|
||||
function sendEmbedMessage(postId) {
|
||||
const input = document.getElementById(`embedInput-${postId}`);
|
||||
const content = input.value.trim();
|
||||
|
||||
if (!content) return;
|
||||
|
||||
// Disable input while sending
|
||||
input.disabled = true;
|
||||
|
||||
fetch('/api/v1/chat/embed/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
post_id: postId || null
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
input.value = '';
|
||||
// Reload messages to show the new one
|
||||
loadEmbedMessages(postId);
|
||||
} else {
|
||||
alert('Error sending message: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to send message');
|
||||
})
|
||||
.finally(() => {
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-load messages when widget is first expanded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click handlers to all chat embed widgets
|
||||
document.querySelectorAll('.chat-embed-widget').forEach(widget => {
|
||||
widget.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.chat-embed-header')) {
|
||||
const postId = widget.dataset.postId;
|
||||
if (postId && widget.classList.contains('expanded')) {
|
||||
loadEmbedMessages(postId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
272
app/templates/chat/index.html
Normal file
272
app/templates/chat/index.html
Normal file
@@ -0,0 +1,272 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Chat - Community Discussions{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 pt-16">
|
||||
<!-- Header Section -->
|
||||
<div class="relative overflow-hidden py-16">
|
||||
<div class="absolute inset-0 bg-black/20"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-white/20">
|
||||
<h1 class="text-4xl font-bold text-white mb-4">
|
||||
<i class="fas fa-comments mr-3"></i>Community Chat
|
||||
</h1>
|
||||
<p class="text-blue-200 text-lg">Connect with fellow motorcycle adventurers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 -mt-8">
|
||||
<!-- Community Guidelines Card -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden mb-8">
|
||||
<div class="bg-gradient-to-r from-green-600 to-emerald-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-shield-alt text-3xl mr-4"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Community Guidelines</h3>
|
||||
<p class="text-green-100">Keep our community safe and welcoming</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 bg-green-50">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2 mt-1"></i>
|
||||
<span>Be respectful to all community members</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2 mt-1"></i>
|
||||
<span>Share motorcycle adventures and tips</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2 mt-1"></i>
|
||||
<span>Help others with technical questions</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-life-ring text-3xl mr-4"></i>
|
||||
<div>
|
||||
<h4 class="text-lg font-bold">Get Support</h4>
|
||||
<p class="text-blue-100 text-sm">Need help or password reset?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 mb-4">Contact administrators for support or password reset</p>
|
||||
<a href="{{ url_for('chat.support') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200">
|
||||
<i class="fas fa-headset mr-2"></i>Get Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
||||
<div class="bg-gradient-to-r from-green-600 to-emerald-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-plus-circle text-3xl mr-4"></i>
|
||||
<div>
|
||||
<h4 class="text-lg font-bold">Create Room</h4>
|
||||
<p class="text-green-100 text-sm">Start a new discussion</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 mb-4">Start a new chat room on any motorcycle topic</p>
|
||||
<a href="{{ url_for('chat.create_room_form') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white font-semibold rounded-lg hover:from-green-700 hover:to-emerald-700 transition-all duration-200">
|
||||
<i class="fas fa-plus mr-2"></i>Create Room
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
||||
<div class="bg-gradient-to-r from-orange-600 to-red-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-key text-3xl mr-4"></i>
|
||||
<div>
|
||||
<h4 class="text-lg font-bold">Password Reset</h4>
|
||||
<p class="text-orange-100 text-sm">Forgot your password?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 mb-4">Request a password reset from administrators</p>
|
||||
<a href="{{ url_for('auth.forgot_password') }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-orange-600 to-red-600 text-white font-semibold rounded-lg hover:from-orange-700 hover:to-red-700 transition-all duration-200">
|
||||
<i class="fas fa-unlock-alt mr-2"></i>Reset Password
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user_rooms %}
|
||||
<!-- Your Recent Chats -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-white mb-6 flex items-center">
|
||||
<i class="fas fa-history mr-3"></i>Your Recent Chats
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for room in user_rooms %}
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
||||
<div class="bg-gradient-to-r
|
||||
{% if room.room_type == 'support' %}from-purple-600 to-indigo-600
|
||||
{% elif room.room_type == 'public' %}from-blue-600 to-cyan-600
|
||||
{% elif room.room_type == 'group' %}from-green-600 to-teal-600
|
||||
{% else %}from-gray-600 to-slate-600{% endif %} p-6">
|
||||
<div class="flex items-center justify-between text-white">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">{{ room.name }}</h3>
|
||||
<p class="text-sm opacity-80">{{ room.room_type.replace('_', ' ').title() }}</p>
|
||||
</div>
|
||||
<i class="fas
|
||||
{% if room.room_type == 'support' %}fa-headset
|
||||
{% elif room.room_type == 'public' %}fa-users
|
||||
{% elif room.room_type == 'group' %}fa-user-friends
|
||||
{% else %}fa-comments{% endif %} text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 mb-4">{{ room.description or 'No description available' }}</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">
|
||||
<i class="fas fa-clock mr-1"></i>
|
||||
{{ room.last_activity.strftime('%m/%d %H:%M') if room.last_activity else 'No activity' }}
|
||||
</span>
|
||||
<a href="{{ url_for('chat.room', room_id=room.id) }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>Join Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if public_rooms %}
|
||||
<!-- Public Chat Rooms -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-white mb-6 flex items-center">
|
||||
<i class="fas fa-globe mr-3"></i>Public Chat Rooms
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for room in public_rooms %}
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
||||
<div class="bg-gradient-to-r
|
||||
{% if room.room_type == 'support' %}from-purple-600 to-indigo-600
|
||||
{% elif room.room_type == 'public' %}from-blue-600 to-cyan-600
|
||||
{% elif room.room_type == 'group' %}from-green-600 to-teal-600
|
||||
{% else %}from-gray-600 to-slate-600{% endif %} p-6">
|
||||
<div class="flex items-center justify-between text-white">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">{{ room.name }}</h3>
|
||||
<p class="text-sm opacity-80">{{ room.room_type.replace('_', ' ').title() }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<i class="fas
|
||||
{% if room.room_type == 'support' %}fa-headset
|
||||
{% elif room.room_type == 'public' %}fa-users
|
||||
{% elif room.room_type == 'group' %}fa-user-friends
|
||||
{% else %}fa-comments{% endif %} text-2xl"></i>
|
||||
<p class="text-xs mt-1">{{ room.participants.count() }} members</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-gray-600 mb-3">{{ room.description or 'Join the conversation!' }}</p>
|
||||
{% if room.related_post %}
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
|
||||
<p class="text-sm text-blue-700">
|
||||
<i class="fas fa-link mr-1"></i>Related to: {{ room.related_post.title }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-500">
|
||||
<i class="fas fa-clock mr-1"></i>
|
||||
{{ room.last_activity.strftime('%m/%d %H:%M') if room.last_activity else 'No activity' }}
|
||||
</span>
|
||||
<a href="{{ url_for('chat.room', room_id=room.id) }}"
|
||||
class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>Join Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not user_rooms and not public_rooms %}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-12 border border-white/20">
|
||||
<i class="fas fa-comments text-6xl text-white/50 mb-6"></i>
|
||||
<h3 class="text-2xl font-bold text-white mb-4">No chat rooms available</h3>
|
||||
<p class="text-blue-200 mb-8">Be the first to start a conversation!</p>
|
||||
<a href="{{ url_for('chat.create_room_form') }}"
|
||||
class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-green-600 to-emerald-600 text-white font-semibold rounded-lg hover:from-green-700 hover:to-emerald-700 transition-all duration-200">
|
||||
<i class="fas fa-plus mr-2"></i>Create First Room
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Mobile app detection and API guidance
|
||||
if (window.ReactNativeWebView || window.flutter_inappwebview) {
|
||||
console.log('Mobile app detected - use API endpoints for better performance');
|
||||
document.body.classList.add('mobile-app-view');
|
||||
}
|
||||
|
||||
// Auto-refresh room list every 60 seconds (increased from 30s to reduce server load)
|
||||
let refreshInterval;
|
||||
function startAutoRefresh() {
|
||||
refreshInterval = setInterval(() => {
|
||||
if (document.visibilityState === 'visible' && !document.hidden) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
// Pause refresh when page is hidden
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
clearInterval(refreshInterval);
|
||||
} else {
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Start auto-refresh on page load
|
||||
startAutoRefresh();
|
||||
|
||||
// Smooth scrolling for better UX
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
413
app/templates/chat/room.html
Normal file
413
app/templates/chat/room.html
Normal file
@@ -0,0 +1,413 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ room.name }} - Chat{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.chat-room-container {
|
||||
height: calc(100vh - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.messages-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1rem;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-author {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 70%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.own-message {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.own-message .message-content {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.system-message .message-content {
|
||||
background: #e9ecef;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-input-area {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 25px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.participants-sidebar {
|
||||
width: 250px;
|
||||
background: white;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 10px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.participant-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.participant-role {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.participants-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="chat-room-container">
|
||||
<div class="chat-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ room.name }}</h4>
|
||||
<small class="text-muted">{{ room.description or 'No description' }}</small>
|
||||
{% if room.related_post %}
|
||||
<br><small class="text-primary">Related to: <a href="#" class="text-primary">{{ room.related_post.title }}</a></small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('chat.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Chats
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-main">
|
||||
<div class="messages-container">
|
||||
<div class="messages-area" id="messagesArea">
|
||||
{% for message in messages %}
|
||||
<div class="message {% if message.user_id == current_user.id %}own-message{% endif %} {% if message.message_type == 'system' %}system-message{% endif %}">
|
||||
{% if message.message_type != 'system' %}
|
||||
<div class="message-header">
|
||||
<span class="message-author">{{ message.user.nickname }}</span>
|
||||
{% if message.user.is_admin %}
|
||||
<span class="admin-badge">ADMIN</span>
|
||||
{% endif %}
|
||||
<span class="message-time">{{ message.created_at.strftime('%H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="message-content">
|
||||
{{ message.content }}
|
||||
{% if message.is_edited %}
|
||||
<small class="text-muted"> (edited)</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="message-input-area">
|
||||
<form id="messageForm" class="d-flex gap-2">
|
||||
<input type="text"
|
||||
id="messageInput"
|
||||
class="form-control message-input"
|
||||
placeholder="Type your message..."
|
||||
maxlength="2000"
|
||||
autocomplete="off">
|
||||
<button type="submit" class="send-button">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</form>
|
||||
<small class="text-muted">Press Enter to send • Max 2000 characters</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participants-sidebar">
|
||||
<h6 class="mb-3">Participants ({{ participants|length }})</h6>
|
||||
{% for participant in participants %}
|
||||
<div class="participant-item">
|
||||
<div class="participant-avatar">
|
||||
{{ participant.user.nickname[0].upper() }}
|
||||
</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">{{ participant.user.nickname }}</div>
|
||||
<div class="participant-role">
|
||||
{{ participant.role.title() }}
|
||||
{% if participant.user.is_admin %}• Admin{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const roomId = {{ room.id }};
|
||||
const currentUserId = {{ current_user.id }};
|
||||
let lastMessageId = {{ messages[-1].id if messages else 0 }};
|
||||
|
||||
// Message form handling
|
||||
document.getElementById('messageForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
});
|
||||
|
||||
// Enter key handling
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const content = input.value.trim();
|
||||
|
||||
if (!content) return;
|
||||
|
||||
// Disable input while sending
|
||||
input.disabled = true;
|
||||
|
||||
fetch(`/api/v1/chat/rooms/${roomId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
message_type: 'text'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
input.value = '';
|
||||
addMessageToUI(data.message);
|
||||
scrollToBottom();
|
||||
} else {
|
||||
alert('Error sending message: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to send message');
|
||||
})
|
||||
.finally(() => {
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function addMessageToUI(message) {
|
||||
const messagesArea = document.getElementById('messagesArea');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${message.user.id === currentUserId ? 'own-message' : ''} ${message.message_type === 'system' ? 'system-message' : ''}`;
|
||||
|
||||
let messageHTML = '';
|
||||
if (message.message_type !== 'system') {
|
||||
messageHTML += `
|
||||
<div class="message-header">
|
||||
<span class="message-author">${message.user.nickname}</span>
|
||||
${message.user.is_admin ? '<span class="admin-badge">ADMIN</span>' : ''}
|
||||
<span class="message-time">${new Date(message.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageHTML += `
|
||||
<div class="message-content">
|
||||
${message.content}
|
||||
${message.is_edited ? '<small class="text-muted"> (edited)</small>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
messageDiv.innerHTML = messageHTML;
|
||||
messagesArea.appendChild(messageDiv);
|
||||
|
||||
lastMessageId = message.id;
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const messagesArea = document.getElementById('messagesArea');
|
||||
messagesArea.scrollTop = messagesArea.scrollHeight;
|
||||
}
|
||||
|
||||
function loadNewMessages() {
|
||||
fetch(`/api/v1/chat/rooms/${roomId}/messages?after=${lastMessageId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.messages.length > 0) {
|
||||
data.messages.forEach(message => {
|
||||
addMessageToUI(message);
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading new messages:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom on load
|
||||
scrollToBottom();
|
||||
|
||||
// Poll for new messages every 3 seconds
|
||||
setInterval(loadNewMessages, 3000);
|
||||
|
||||
// Auto-focus on message input
|
||||
document.getElementById('messageInput').focus();
|
||||
|
||||
// Mobile app integration
|
||||
if (window.ReactNativeWebView) {
|
||||
// React Native WebView integration
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
type: 'chat_room_opened',
|
||||
roomId: roomId,
|
||||
roomName: '{{ room.name }}'
|
||||
}));
|
||||
}
|
||||
|
||||
// Flutter WebView integration
|
||||
if (window.flutter_inappwebview) {
|
||||
window.flutter_inappwebview.callHandler('chatRoomOpened', {
|
||||
roomId: roomId,
|
||||
roomName: '{{ room.name }}'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
449
app/templates/chat/support.html
Normal file
449
app/templates/chat/support.html
Normal file
@@ -0,0 +1,449 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin Support - Chat{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.support-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.support-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.support-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.support-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border: 2px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
border-color: #667eea;
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.15);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.support-form {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
border: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.priority-selector {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.priority-option {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.priority-option.active {
|
||||
border-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.priority-low { border-left: 4px solid #28a745; }
|
||||
.priority-medium { border-left: 4px solid #ffc107; }
|
||||
.priority-high { border-left: 4px solid #dc3545; }
|
||||
|
||||
.recent-tickets {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.ticket-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ticket-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.status-open { background: #28a745; }
|
||||
.status-pending { background: #ffc107; }
|
||||
.status-closed { background: #6c757d; }
|
||||
|
||||
.ticket-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ticket-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 2rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.support-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.support-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.priority-selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="support-container">
|
||||
<div class="container">
|
||||
<div class="support-card">
|
||||
<div class="support-header">
|
||||
<div class="support-icon">
|
||||
<i class="fas fa-headset"></i>
|
||||
</div>
|
||||
<h2>Admin Support</h2>
|
||||
<p class="text-muted">Get help from our administrators for account issues, password resets, and technical support</p>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<a href="#" class="action-card" onclick="startPasswordReset()">
|
||||
<div class="action-icon">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<h5>Password Reset</h5>
|
||||
<p class="text-muted">Reset your account password with admin assistance</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-card" onclick="startAccountIssue()">
|
||||
<div class="action-icon">
|
||||
<i class="fas fa-user-cog"></i>
|
||||
</div>
|
||||
<h5>Account Issues</h5>
|
||||
<p class="text-muted">Login problems, profile updates, and account settings</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-card" onclick="startTechnicalSupport()">
|
||||
<div class="action-icon">
|
||||
<i class="fas fa-tools"></i>
|
||||
</div>
|
||||
<h5>Technical Support</h5>
|
||||
<p class="text-muted">App bugs, feature requests, and technical assistance</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-card" onclick="startGeneralInquiry()">
|
||||
<div class="action-icon">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
</div>
|
||||
<h5>General Inquiry</h5>
|
||||
<p class="text-muted">Questions about features, policies, or general help</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="support-form">
|
||||
<h4 class="mb-3">Create Support Ticket</h4>
|
||||
<form id="supportForm">
|
||||
<div class="form-group">
|
||||
<label for="subject" class="form-label">Subject</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject" required
|
||||
placeholder="Brief description of your issue">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<select class="form-control" id="category" name="category" required>
|
||||
<option value="">Select a category</option>
|
||||
<option value="password_reset">Password Reset</option>
|
||||
<option value="account_issues">Account Issues</option>
|
||||
<option value="technical_support">Technical Support</option>
|
||||
<option value="general_inquiry">General Inquiry</option>
|
||||
<option value="bug_report">Bug Report</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Priority</label>
|
||||
<div class="priority-selector">
|
||||
<div class="priority-option priority-low" data-priority="low">
|
||||
<strong>Low</strong><br>
|
||||
<small>General questions</small>
|
||||
</div>
|
||||
<div class="priority-option priority-medium active" data-priority="medium">
|
||||
<strong>Medium</strong><br>
|
||||
<small>Account issues</small>
|
||||
</div>
|
||||
<div class="priority-option priority-high" data-priority="high">
|
||||
<strong>High</strong><br>
|
||||
<small>Urgent problems</small>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="priority" name="priority" value="medium">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6" required
|
||||
placeholder="Please provide detailed information about your issue..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contactMethod" class="form-label">Preferred Contact Method</label>
|
||||
<select class="form-control" id="contactMethod" name="contact_method">
|
||||
<option value="chat">Chat (Recommended)</option>
|
||||
<option value="email">Email Notification</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-gradient btn-lg">
|
||||
<i class="fas fa-paper-plane me-2"></i>
|
||||
Submit Support Request
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="recent-tickets">
|
||||
<h5 class="mb-3">Your Recent Tickets</h5>
|
||||
{% if recent_tickets %}
|
||||
{% for ticket in recent_tickets %}
|
||||
<div class="ticket-item">
|
||||
<div class="ticket-status status-{{ ticket.status }}"></div>
|
||||
<div class="ticket-info">
|
||||
<div class="ticket-title">{{ ticket.subject }}</div>
|
||||
<div class="ticket-meta">
|
||||
{{ ticket.created_at.strftime('%b %d, %Y') }} •
|
||||
{{ ticket.category.replace('_', ' ').title() }}
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('chat.room', room_id=ticket.chat_room_id) }}" class="btn btn-sm btn-outline-primary">
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted text-center">No recent support tickets</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="recent-tickets mt-3">
|
||||
<h6 class="mb-3">Support Information</h6>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Response Time:</strong> Most tickets are answered within 2-4 hours during business hours.
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-clock"></i>
|
||||
<strong>Business Hours:</strong> Monday-Friday, 9 AM - 6 PM (Local Time)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Priority selector handling
|
||||
document.querySelectorAll('.priority-option').forEach(option => {
|
||||
option.addEventListener('click', function() {
|
||||
document.querySelectorAll('.priority-option').forEach(opt => opt.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById('priority').value = this.dataset.priority;
|
||||
});
|
||||
});
|
||||
|
||||
// Quick action handlers
|
||||
function startPasswordReset() {
|
||||
document.getElementById('subject').value = 'Password Reset Request';
|
||||
document.getElementById('category').value = 'password_reset';
|
||||
document.getElementById('description').value = 'I need help resetting my password. ';
|
||||
document.getElementById('description').focus();
|
||||
}
|
||||
|
||||
function startAccountIssue() {
|
||||
document.getElementById('subject').value = 'Account Issue';
|
||||
document.getElementById('category').value = 'account_issues';
|
||||
document.getElementById('description').value = 'I am experiencing issues with my account: ';
|
||||
document.getElementById('description').focus();
|
||||
}
|
||||
|
||||
function startTechnicalSupport() {
|
||||
document.getElementById('subject').value = 'Technical Support Request';
|
||||
document.getElementById('category').value = 'technical_support';
|
||||
document.getElementById('description').value = 'I need technical assistance with: ';
|
||||
document.getElementById('description').focus();
|
||||
}
|
||||
|
||||
function startGeneralInquiry() {
|
||||
document.getElementById('subject').value = 'General Inquiry';
|
||||
document.getElementById('category').value = 'general_inquiry';
|
||||
document.getElementById('description').value = 'I have a question about: ';
|
||||
document.getElementById('description').focus();
|
||||
}
|
||||
|
||||
// Support form submission
|
||||
document.getElementById('supportForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const submitButton = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.innerHTML;
|
||||
|
||||
// Disable button and show loading
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Creating Ticket...';
|
||||
|
||||
fetch('/api/v1/chat/support/create', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Support ticket created successfully! You will be redirected to the chat room.');
|
||||
window.location.href = `/chat/room/${data.room_id}`;
|
||||
} else {
|
||||
alert('Error creating support ticket: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to create support ticket. Please try again.');
|
||||
})
|
||||
.finally(() => {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile app integration
|
||||
if (window.ReactNativeWebView) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
type: 'support_page_opened'
|
||||
}));
|
||||
}
|
||||
|
||||
if (window.flutter_inappwebview) {
|
||||
window.flutter_inappwebview.callHandler('supportPageOpened');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user