- 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
445 lines
22 KiB
HTML
445 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}My Profile - {{ current_user.nickname }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 pt-16">
|
|
<!-- Profile Header -->
|
|
<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">
|
|
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-white/20">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div class="flex items-center space-x-6">
|
|
<div class="w-20 h-20 bg-gradient-to-r from-orange-500 to-red-600 rounded-full flex items-center justify-center text-white font-bold text-3xl">
|
|
{{ current_user.nickname[0].upper() }}
|
|
</div>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">{{ current_user.nickname }}</h1>
|
|
<p class="text-blue-200 mb-1">
|
|
<i class="fas fa-envelope mr-2"></i>{{ current_user.email }}
|
|
</p>
|
|
<p class="text-blue-200">
|
|
<i class="fas fa-calendar mr-2"></i>Riding since {{ current_user.created_at.strftime('%B %Y') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<a href="{{ url_for('community.new_post') }}"
|
|
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 transform hover:scale-105">
|
|
<i class="fas fa-plus mr-2"></i>Share New Adventure
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 -mt-8">
|
|
<!-- Adventure Statistics -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
<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-check-circle text-3xl mr-4"></i>
|
|
<div>
|
|
<div class="text-3xl font-bold">{{ published_count }}</div>
|
|
<div class="text-green-100">Published Adventures</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-4">
|
|
<p class="text-gray-600 text-sm">Visible to the community</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
<div class="bg-gradient-to-r from-yellow-600 to-orange-600 p-6">
|
|
<div class="flex items-center text-white">
|
|
<i class="fas fa-clock text-3xl mr-4"></i>
|
|
<div>
|
|
<div class="text-3xl font-bold">{{ pending_count }}</div>
|
|
<div class="text-yellow-100">Awaiting Review</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-4">
|
|
<p class="text-gray-600 text-sm">Pending admin approval</p>
|
|
</div>
|
|
</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">
|
|
<i class="fas fa-mountain text-3xl text-blue-600 mr-4"></i>
|
|
<div>
|
|
<h2 class="text-2xl font-bold text-gray-900">My Adventure Collection</h2>
|
|
<p class="text-gray-600">Manage and share your motorcycle adventures</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Adventures Grid -->
|
|
{% if posts.items %}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{% for post in posts.items %}
|
|
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
|
|
<!-- Adventure Image -->
|
|
{% set cover_image = post.images | selectattr('is_cover', 'equalto', True) | first %}
|
|
{% if cover_image %}
|
|
<div class="relative">
|
|
<img src="{{ cover_image.get_thumbnail_url() }}"
|
|
alt="{{ post.title }}"
|
|
class="w-full h-48 object-cover">
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
|
<div class="absolute top-3 right-3">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold
|
|
{{ 'bg-green-500 text-white' if post.published else 'bg-yellow-500 text-black' }}">
|
|
{% if post.published %}
|
|
<i class="fas fa-check-circle mr-1"></i>Live
|
|
{% else %}
|
|
<i class="fas fa-clock mr-1"></i>Review
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="h-48 bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center relative">
|
|
<i class="fas fa-mountain text-white text-6xl opacity-50"></i>
|
|
<div class="absolute top-3 right-3">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold
|
|
{{ 'bg-green-500 text-white' if post.published else 'bg-yellow-500 text-black' }}">
|
|
{% if post.published %}
|
|
<i class="fas fa-check-circle mr-1"></i>Live
|
|
{% else %}
|
|
<i class="fas fa-clock mr-1"></i>Review
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="p-6">
|
|
<!-- Title and Subtitle -->
|
|
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ post.title }}</h3>
|
|
{% if post.subtitle %}
|
|
<p class="text-gray-600 text-sm mb-3">{{ post.subtitle }}</p>
|
|
{% endif %}
|
|
|
|
<!-- Difficulty Badge -->
|
|
<div class="mb-4">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full bg-gradient-to-r from-yellow-500 to-orange-500 text-white text-sm font-semibold">
|
|
{% for i in range(post.difficulty) %}
|
|
<i class="fas fa-star mr-1"></i>
|
|
{% endfor %}
|
|
{{ post.get_difficulty_label() }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Content Preview -->
|
|
<p class="text-gray-600 text-sm mb-4 h-12 overflow-hidden">
|
|
{{ (post.content[:100] + '...') if post.content|length > 100 else post.content }}
|
|
</p>
|
|
|
|
<!-- Adventure Metadata -->
|
|
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
|
<div class="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<i class="fas fa-calendar text-blue-500 mb-1"></i>
|
|
<div class="text-sm font-semibold">{{ post.created_at.strftime('%m/%d') }}</div>
|
|
<div class="text-xs text-gray-500">Created</div>
|
|
</div>
|
|
<div>
|
|
<i class="fas fa-images text-green-500 mb-1"></i>
|
|
<div class="text-sm font-semibold">{{ post.images.count() }}</div>
|
|
<div class="text-xs text-gray-500">Photos</div>
|
|
</div>
|
|
<div>
|
|
<i class="fas fa-route text-orange-500 mb-1"></i>
|
|
<div class="text-sm font-semibold">{{ post.gpx_files.count() }}</div>
|
|
<div class="text-xs text-gray-500">Routes</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="space-y-2">
|
|
{% if post.published %}
|
|
<a href="{{ url_for('community.post_detail', id=post.id) }}"
|
|
class="block w-full text-center px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-all duration-200">
|
|
<i class="fas fa-eye mr-2"></i>View Live
|
|
</a>
|
|
{% endif %}
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<a href="{{ url_for('community.edit_post', id=post.id) }}"
|
|
class="text-center px-3 py-2 bg-yellow-500 text-white font-semibold rounded-lg hover:bg-yellow-600 transition-all duration-200">
|
|
<i class="fas fa-edit mr-1"></i>Edit
|
|
</a>
|
|
<button type="button"
|
|
class="px-3 py-2 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition-all duration-200"
|
|
data-post-id="{{ post.id }}"
|
|
data-post-title="{{ post.title }}"
|
|
onclick="confirmDelete(this)">
|
|
<i class="fas fa-trash mr-1"></i>Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<!-- No Adventures State -->
|
|
<div class="bg-white rounded-2xl shadow-xl p-12 text-center">
|
|
<i class="fas fa-mountain text-6xl text-gray-400 mb-6"></i>
|
|
<h3 class="text-2xl font-bold text-gray-900 mb-4">Your Adventure Journey Awaits!</h3>
|
|
<p class="text-gray-600 mb-8 max-w-md mx-auto">
|
|
You haven't shared any motorcycle adventures yet. Every great ride has a story worth telling!
|
|
</p>
|
|
<a href="{{ url_for('community.new_post') }}"
|
|
class="inline-flex items-center px-8 py-4 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 transform hover:scale-105">
|
|
<i class="fas fa-plus mr-2"></i>Share Your First Adventure
|
|
</a>
|
|
<div class="mt-6">
|
|
<p class="text-sm text-gray-500">
|
|
<i class="fas fa-lightbulb mr-1"></i>
|
|
Share photos, GPS tracks, and stories of your rides to inspire the community
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Pagination -->
|
|
{% if posts.pages > 1 %}
|
|
<div class="flex justify-center mt-8">
|
|
<div class="bg-white rounded-lg shadow-lg px-6 py-4">
|
|
<div class="flex items-center space-x-4">
|
|
{% if posts.has_prev %}
|
|
<a href="{{ url_for('community.profile', page=posts.prev_num) }}"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200">
|
|
<i class="fas fa-chevron-left mr-1"></i>Previous
|
|
</a>
|
|
{% endif %}
|
|
|
|
<span class="text-gray-600">
|
|
Page {{ posts.page }} of {{ posts.pages }}
|
|
</span>
|
|
|
|
{% if posts.has_next %}
|
|
<a href="{{ url_for('community.profile', page=posts.next_num) }}"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200">
|
|
Next<i class="fas fa-chevron-right ml-1"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-2xl p-8 max-w-md mx-4">
|
|
<div class="text-center">
|
|
<i class="fas fa-exclamation-triangle text-red-500 text-4xl mb-4"></i>
|
|
<h3 class="text-xl font-bold text-gray-900 mb-4">Delete Adventure</h3>
|
|
<p class="text-gray-600 mb-6">
|
|
Are you sure you want to delete "<span id="deletePostTitle" class="font-semibold"></span>"?
|
|
This action cannot be undone.
|
|
</p>
|
|
<div class="flex space-x-4">
|
|
<button onclick="closeDeleteModal()"
|
|
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-all duration-200">
|
|
Cancel
|
|
</button>
|
|
<form id="deleteForm" method="POST" class="flex-1">
|
|
<button type="submit"
|
|
class="w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-all duration-200">
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function confirmDelete(button) {
|
|
const postId = button.getAttribute('data-post-id');
|
|
const postTitle = button.getAttribute('data-post-title');
|
|
|
|
document.getElementById('deletePostTitle').textContent = postTitle;
|
|
document.getElementById('deleteForm').action = `/community/delete-post/${postId}`;
|
|
document.getElementById('deleteModal').classList.remove('hidden');
|
|
}
|
|
|
|
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) {
|
|
closeDeleteModal();
|
|
}
|
|
});
|
|
|
|
// Close modal on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeDeleteModal();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|