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:
ske087
2025-08-09 20:44:25 +03:00
parent d1e2b95678
commit 1661f5f588
14 changed files with 2742 additions and 34 deletions

View File

@@ -1,27 +1,67 @@
{% extends "base.html" %}
{% block title %}Forgot Password{% endblock %}
{% block title %}Password Reset Request{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50 py-20">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-blue-500 via-purple-500 to-teal-500 p-6 text-white text-center">
<h2 class="text-2xl font-bold">Forgot your password?</h2>
<p class="text-blue-100 mt-1">Enter your email to receive a reset link.</p>
</div>
<form method="POST" class="p-6 space-y-6">
{{ form.hidden_tag() }}
<div>
{{ form.email.label(class="block text-sm font-medium text-gray-700 mb-1") }}
{{ form.email(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent") }}
{% if form.email.errors %}
<div class="text-red-500 text-sm mt-1">
{% for error in form.email.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 py-20">
<div class="max-w-md mx-auto">
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="bg-gradient-to-r from-orange-600 to-red-600 p-8 text-white text-center">
<i class="fas fa-key text-4xl mb-4"></i>
<h2 class="text-2xl font-bold">Password Reset Request</h2>
<p class="text-orange-100 mt-2">We'll help you get back into your account</p>
</div>
{{ form.submit(class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-2 px-4 rounded-lg hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition font-medium") }}
</form>
<div class="p-8">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<i class="fas fa-info-circle text-blue-500 mr-3 mt-1"></i>
<div class="text-sm text-blue-700">
<p class="font-semibold mb-1">How it works:</p>
<p>Enter your email address and we'll send a password reset request to our administrators. They will contact you directly to help reset your password securely.</p>
</div>
</div>
</div>
<form method="POST" class="space-y-6">
{{ form.hidden_tag() }}
<div>
{{ form.email.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400"></i>
</div>
{{ form.email(class="w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-200", placeholder="Enter your email address") }}
</div>
{% if form.email.errors %}
<div class="text-red-500 text-sm mt-2">
{% for error in form.email.errors %}
<p><i class="fas fa-exclamation-circle mr-1"></i>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="w-full bg-gradient-to-r from-orange-600 to-red-600 text-white py-3 px-6 rounded-lg hover:from-orange-700 hover:to-red-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition-all duration-200 font-semibold text-lg") }}
</form>
<div class="mt-8 pt-6 border-t border-gray-200 text-center">
<p class="text-sm text-gray-600 mb-4">Remember your password?</p>
<a href="{{ url_for('auth.login') }}"
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
<i class="fas fa-arrow-left mr-2"></i>Back to Login
</a>
</div>
<div class="mt-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex items-start">
<i class="fas fa-shield-alt text-green-500 mr-3 mt-1"></i>
<div class="text-sm text-green-700">
<p class="font-semibold mb-1">Security Note:</p>
<p>Our administrators will verify your identity before resetting your password to keep your account secure.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -21,6 +21,9 @@
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="{{ url_for('community.index') }}" class="text-white hover:text-teal-200 transition font-semibold">🏍️ Adventures</a>
<a href="{{ url_for('chat.index') }}" class="text-white hover:text-purple-200 transition">
<i class="fas fa-comments mr-1"></i>Chat
</a>
<a href="{{ url_for('main.index') }}#accommodation" class="text-white hover:text-purple-200 transition">Accommodation</a>
{% if current_user.is_authenticated %}
{% if not current_user.is_admin %}
@@ -59,6 +62,9 @@
<a href="{{ url_for('main.index') }}#about" class="text-white block px-3 py-2 hover:bg-blue-600 rounded">About</a>
<a href="{{ url_for('main.index') }}#accommodation" class="text-white block px-3 py-2 hover:bg-purple-600 rounded">Accommodation</a>
<a href="{{ url_for('community.index') }}" class="text-white block px-3 py-2 hover:bg-teal-600 rounded">Stories & Tracks</a>
<a href="{{ url_for('chat.index') }}" class="text-white block px-3 py-2 hover:bg-purple-600 rounded">
<i class="fas fa-comments mr-2"></i>Chat
</a>
{% if current_user.is_authenticated %}
{% if not current_user.is_admin %}
<a href="{{ url_for('community.new_post') }}" class="text-white block px-3 py-2 hover:bg-green-600 rounded">
@@ -176,5 +182,7 @@
});
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View 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>

View 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 %}

View 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 %}

View 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 %}

View File

@@ -642,6 +642,9 @@
</div>
</div>
<!-- Chat Discussion Widget -->
{% include 'chat/embed.html' %}
{% endblock %}
{% block scripts %}

View File

@@ -68,6 +68,92 @@
</div>
</div>
<!-- Change Password Card -->
<div class="bg-white rounded-2xl shadow-xl overflow-hidden mb-8">
<div class="cursor-pointer transition-all duration-200 hover:bg-gray-50" onclick="togglePasswordCard()">
<div class="p-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-key text-3xl text-purple-600 mr-4"></i>
<div>
<h3 class="text-xl font-bold text-gray-900">Change Password</h3>
<p class="text-gray-600">Update your account password for security</p>
</div>
</div>
<i id="passwordCardToggle" class="fas fa-chevron-down text-gray-400 text-xl transition-transform duration-200"></i>
</div>
</div>
</div>
<!-- Password Change Form (Initially Hidden) -->
<div id="passwordChangeForm" class="hidden border-t border-gray-200">
<div class="p-6 bg-gray-50">
<form id="changePasswordForm" method="POST" action="{{ url_for('auth.change_password') }}" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Current Password -->
<div>
<label for="current_password" class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-lock mr-1"></i>Current Password
</label>
<input type="password"
id="current_password"
name="current_password"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200"
placeholder="Enter current password">
</div>
<!-- New Password -->
<div>
<label for="new_password" class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-key mr-1"></i>New Password
</label>
<input type="password"
id="new_password"
name="new_password"
required
minlength="6"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200"
placeholder="Enter new password">
</div>
<!-- Confirm New Password -->
<div>
<label for="confirm_password" class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-check-circle mr-1"></i>Confirm Password
</label>
<input type="password"
id="confirm_password"
name="confirm_password"
required
minlength="6"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200"
placeholder="Confirm new password">
</div>
</div>
<!-- Password Requirements -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 class="font-semibold text-blue-800 mb-2">Password Requirements:</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li><i class="fas fa-check text-green-500 mr-1"></i>At least 6 characters long</li>
<li><i class="fas fa-info-circle text-blue-500 mr-1"></i>Use a unique password you don't use elsewhere</li>
<li><i class="fas fa-shield-alt text-green-500 mr-1"></i>Consider using a mix of letters, numbers, and symbols</li>
</ul>
</div>
<!-- Submit Button -->
<div class="flex justify-end">
<button type="submit"
class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-semibold rounded-lg hover:from-purple-700 hover:to-blue-700 transition-all duration-200 transform hover:scale-105">
<i class="fas fa-save mr-2"></i>Update Password
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Adventures Collection Header -->
<div class="bg-white rounded-2xl shadow-xl p-6 mb-8">
<div class="flex items-center">
@@ -280,6 +366,67 @@ function closeDeleteModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
// Password card toggle functionality
function togglePasswordCard() {
const form = document.getElementById('passwordChangeForm');
const toggle = document.getElementById('passwordCardToggle');
if (form && toggle) {
if (form.classList.contains('hidden')) {
form.classList.remove('hidden');
toggle.classList.remove('fa-chevron-down');
toggle.classList.add('fa-chevron-up');
} else {
form.classList.add('hidden');
toggle.classList.remove('fa-chevron-up');
toggle.classList.add('fa-chevron-down');
}
}
}
// Make sure DOM is loaded before attaching event listeners
document.addEventListener('DOMContentLoaded', function() {
// Ensure the toggle function is available globally
window.togglePasswordCard = togglePasswordCard;
});
// Password change form validation
document.getElementById('changePasswordForm').addEventListener('submit', function(e) {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
e.preventDefault();
alert('New password and confirm password do not match. Please try again.');
document.getElementById('confirm_password').focus();
return false;
}
if (newPassword.length < 6) {
e.preventDefault();
alert('Password must be at least 6 characters long.');
document.getElementById('new_password').focus();
return false;
}
});
// Real-time password confirmation validation
document.getElementById('confirm_password').addEventListener('input', function() {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = this.value;
if (confirmPassword && newPassword !== confirmPassword) {
this.style.borderColor = '#ef4444';
this.style.backgroundColor = '#fef2f2';
} else if (confirmPassword && newPassword === confirmPassword) {
this.style.borderColor = '#10b981';
this.style.backgroundColor = '#f0fdf4';
} else {
this.style.borderColor = '#d1d5db';
this.style.backgroundColor = '#ffffff';
}
});
// Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {