🎨 Complete Tailwind CSS conversion - Redesigned post detail page with modern gradient backgrounds - Updated profile page with consistent design language - Converted from Bootstrap to Tailwind CSS throughout ✨ New Features & Improvements - Enhanced community post management system - Added admin panel with analytics dashboard - Improved post creation and editing workflows - Interactive GPS map integration with Leaflet.js - Photo gallery with modal view and hover effects - Adventure statistics and metadata display - Like system and community engagement features 🔧 Technical Improvements - Fixed template syntax errors and CSRF token issues - Updated database models and relationships - Enhanced media file management - Improved responsive design patterns - Added proper error handling and validation 📱 Mobile-First Design - Responsive grid layouts - Touch-friendly interactions - Optimized for all screen sizes - Modern card-based UI components 🏍️ Adventure Platform Features - GPS track visualization and statistics - Photo uploads with thumbnail generation - GPX file downloads for registered users - Community comments and discussions - Post approval workflow for admins - Difficulty rating system with star indicators
424 lines
20 KiB
HTML
424 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Edit Adventure - {{ post.title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<!-- Form Section -->
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-primary text-white">
|
|
<h4 class="mb-0">
|
|
<i class="fas fa-edit"></i> Edit Your Adventure
|
|
</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="editPostForm" method="POST" enctype="multipart/form-data" action="{{ url_for('community.edit_post', id=post.id) }}">
|
|
<!-- Title -->
|
|
<div class="mb-4">
|
|
<label for="title" class="form-label fw-bold">
|
|
<i class="fas fa-heading text-primary"></i> Adventure Title *
|
|
</label>
|
|
<input type="text" class="form-control form-control-lg" id="title" name="title"
|
|
value="{{ post.title }}" required maxlength="100"
|
|
placeholder="Enter your adventure title">
|
|
<div class="form-text">Make it catchy and descriptive!</div>
|
|
</div>
|
|
|
|
<!-- Subtitle -->
|
|
<div class="mb-4">
|
|
<label for="subtitle" class="form-label fw-bold">
|
|
<i class="fas fa-text-height text-info"></i> Subtitle
|
|
</label>
|
|
<input type="text" class="form-control" id="subtitle" name="subtitle"
|
|
value="{{ post.subtitle or '' }}" maxlength="200"
|
|
placeholder="A brief description of your adventure">
|
|
<div class="form-text">Optional - appears under the main title</div>
|
|
</div>
|
|
|
|
<!-- Difficulty Level -->
|
|
<div class="mb-4">
|
|
<label for="difficulty" class="form-label fw-bold">
|
|
<i class="fas fa-mountain text-warning"></i> Difficulty Level *
|
|
</label>
|
|
<select class="form-select" id="difficulty" name="difficulty" required>
|
|
<option value="">Select difficulty...</option>
|
|
<option value="1" {% if post.difficulty == 1 %}selected{% endif %}>⭐ Easy - Beginner friendly</option>
|
|
<option value="2" {% if post.difficulty == 2 %}selected{% endif %}>⭐⭐ Moderate - Some experience needed</option>
|
|
<option value="3" {% if post.difficulty == 3 %}selected{% endif %}>⭐⭐⭐ Challenging - Good skills required</option>
|
|
<option value="4" {% if post.difficulty == 4 %}selected{% endif %}>⭐⭐⭐⭐ Hard - Advanced riders only</option>
|
|
<option value="5" {% if post.difficulty == 5 %}selected{% endif %}>⭐⭐⭐⭐⭐ Expert - Extreme difficulty</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Current Cover Image -->
|
|
{% if post.images %}
|
|
{% set cover_image = post.images | selectattr('is_cover', 'equalto', True) | first %}
|
|
{% if cover_image %}
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">
|
|
<i class="fas fa-image text-success"></i> Current Cover Photo
|
|
</label>
|
|
<div class="current-cover-preview">
|
|
<img src="{{ cover_image.get_thumbnail_url() }}" alt="Current cover"
|
|
class="img-thumbnail" style="max-width: 200px;">
|
|
<small class="text-muted d-block mt-1">{{ cover_image.original_name }}</small>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<!-- Cover Photo Upload -->
|
|
<div class="mb-4">
|
|
<label for="cover_picture" class="form-label fw-bold">
|
|
<i class="fas fa-camera text-success"></i>
|
|
{% if post.images and post.images | selectattr('is_cover', 'equalto', True) | first %}
|
|
Replace Cover Photo
|
|
{% else %}
|
|
Cover Photo
|
|
{% endif %}
|
|
</label>
|
|
<input type="file" class="form-control" id="cover_picture" name="cover_picture"
|
|
accept="image/*" onchange="previewCoverImage(this)">
|
|
<div class="form-text">
|
|
Optional - Upload a new cover photo to replace the current one
|
|
</div>
|
|
<div id="cover_preview" class="mt-2"></div>
|
|
</div>
|
|
|
|
<!-- Adventure Story/Content -->
|
|
<div class="mb-4">
|
|
<label for="content" class="form-label fw-bold">
|
|
<i class="fas fa-book text-primary"></i> Your Adventure Story *
|
|
</label>
|
|
<textarea class="form-control" id="content" name="content" rows="8" required
|
|
placeholder="Tell us about your adventure... Where did you go? What did you see? Any challenges or highlights?">{{ post.content }}</textarea>
|
|
<div class="form-text">
|
|
<i class="fas fa-info-circle"></i>
|
|
You can use **bold text** and *italic text* in your story!
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current GPX File -->
|
|
{% if post.gpx_files %}
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">
|
|
<i class="fas fa-route text-info"></i> Current GPS Track
|
|
</label>
|
|
{% for gpx_file in post.gpx_files %}
|
|
<div class="current-gpx-file border rounded p-3 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-file-alt text-info me-2"></i>
|
|
<div>
|
|
<strong>{{ gpx_file.original_name }}</strong>
|
|
<small class="text-muted d-block">
|
|
Size: {{ "%.1f"|format(gpx_file.size / 1024) }} KB
|
|
• Uploaded: {{ gpx_file.created_at.strftime('%Y-%m-%d') }}
|
|
</small>
|
|
</div>
|
|
<a href="{{ url_for('community.serve_gpx', post_folder=post.media_folder, filename=gpx_file.filename) }}"
|
|
class="btn btn-sm btn-outline-primary ms-auto">
|
|
<i class="fas fa-download"></i> Download
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- GPX File Upload -->
|
|
<div class="mb-4">
|
|
<label for="gpx_file" class="form-label fw-bold">
|
|
<i class="fas fa-route text-info"></i>
|
|
{% if post.gpx_files %}
|
|
Replace GPS Track File
|
|
{% else %}
|
|
GPS Track File (GPX)
|
|
{% endif %}
|
|
</label>
|
|
<input type="file" class="form-control" id="gpx_file" name="gpx_file"
|
|
accept=".gpx" onchange="validateGpxFile(this)">
|
|
<div class="form-text">
|
|
Optional - Upload a new GPX file to replace the current route
|
|
</div>
|
|
<div id="gpx_info" class="mt-2"></div>
|
|
</div>
|
|
|
|
<!-- Submit Buttons -->
|
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
<a href="{{ url_for('community.profile') }}" class="btn btn-secondary me-md-2">
|
|
<i class="fas fa-arrow-left"></i> Cancel
|
|
</a>
|
|
<button type="button" class="btn btn-info me-md-2" onclick="previewPost()">
|
|
<i class="fas fa-eye"></i> Preview Changes
|
|
</button>
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="fas fa-paper-plane"></i> Update & Resubmit for Review
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Panel -->
|
|
<div class="col-lg-4">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-info-circle"></i> Editing Guidelines
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<h6><i class="fas fa-edit text-primary"></i> What happens after editing?</h6>
|
|
<p class="small">Your updated post will be resubmitted for admin review before being published again.</p>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6><i class="fas fa-image text-success"></i> Photo Guidelines</h6>
|
|
<ul class="small mb-0">
|
|
<li>Use high-quality images (JPEG, PNG)</li>
|
|
<li>Landscape orientation works best for cover photos</li>
|
|
<li>Maximum file size: 10MB</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6><i class="fas fa-route text-info"></i> GPX File Tips</h6>
|
|
<ul class="small mb-0">
|
|
<li>Export from your GPS device or app</li>
|
|
<li>Should contain track points</li>
|
|
<li>Will be displayed on the community map</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6><i class="fas fa-star text-warning"></i> Difficulty Levels</h6>
|
|
<div class="small">
|
|
<div><strong>Easy:</strong> Paved roads, good weather</div>
|
|
<div><strong>Moderate:</strong> Some gravel, hills</div>
|
|
<div><strong>Challenging:</strong> Off-road, technical</div>
|
|
<div><strong>Hard:</strong> Extreme conditions</div>
|
|
<div><strong>Expert:</strong> Dangerous, experts only</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<strong>Note:</strong> Updating your post will reset its status to "pending review."
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="previewModalLabel">
|
|
<i class="fas fa-eye"></i> Post Preview
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="previewContent">
|
|
<!-- Preview content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close Preview</button>
|
|
<button type="button" class="btn btn-success" onclick="submitForm()">
|
|
<i class="fas fa-paper-plane"></i> Looks Good - Update Post
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript -->
|
|
<script>
|
|
function previewCoverImage(input) {
|
|
const preview = document.getElementById('cover_preview');
|
|
preview.innerHTML = '';
|
|
|
|
if (input.files && input.files[0]) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
preview.innerHTML = `
|
|
<div class="mt-2">
|
|
<img src="${e.target.result}" alt="Cover preview" class="img-thumbnail" style="max-width: 200px;">
|
|
<small class="text-success d-block mt-1">✓ New cover photo ready</small>
|
|
</div>
|
|
`;
|
|
};
|
|
reader.readAsDataURL(input.files[0]);
|
|
}
|
|
}
|
|
|
|
function validateGpxFile(input) {
|
|
const info = document.getElementById('gpx_info');
|
|
info.innerHTML = '';
|
|
|
|
if (input.files && input.files[0]) {
|
|
const file = input.files[0];
|
|
if (file.name.toLowerCase().endsWith('.gpx')) {
|
|
info.innerHTML = `
|
|
<div class="alert alert-success">
|
|
<i class="fas fa-check-circle"></i>
|
|
GPX file selected: ${file.name} (${(file.size / 1024).toFixed(1)} KB)
|
|
</div>
|
|
`;
|
|
} else {
|
|
info.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
Please select a valid GPX file
|
|
</div>
|
|
`;
|
|
input.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
function previewPost() {
|
|
// Get form data
|
|
const title = document.getElementById('title').value;
|
|
const subtitle = document.getElementById('subtitle').value;
|
|
const content = document.getElementById('content').value;
|
|
const difficulty = document.getElementById('difficulty').value;
|
|
|
|
// Get difficulty stars
|
|
const difficultyStars = '⭐'.repeat(difficulty);
|
|
const difficultyLabels = {
|
|
'1': 'Easy',
|
|
'2': 'Moderate',
|
|
'3': 'Challenging',
|
|
'4': 'Hard',
|
|
'5': 'Expert'
|
|
};
|
|
|
|
// Format content (simple markdown-like formatting)
|
|
const formattedContent = content
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/\n/g, '<br>');
|
|
|
|
// Generate preview HTML
|
|
const previewHTML = `
|
|
<div class="post-preview">
|
|
<!-- Hero Section -->
|
|
<div class="hero-section bg-primary text-white p-4 rounded mb-4">
|
|
<div class="container">
|
|
<h1 class="display-4 mb-2">${title || 'Your Adventure Title'}</h1>
|
|
${subtitle ? `<p class="lead mb-3">${subtitle}</p>` : ''}
|
|
<div class="d-flex align-items-center">
|
|
<span class="badge bg-warning text-dark me-3">
|
|
${difficultyStars} ${difficultyLabels[difficulty] || 'Select difficulty'}
|
|
</span>
|
|
<small>By {{ current_user.nickname }} • Updated today</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Section -->
|
|
<div class="container">
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="adventure-content">
|
|
<h3>Adventure Story</h3>
|
|
<div class="content-text">
|
|
${formattedContent || '<em>No content provided yet...</em>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="adventure-info">
|
|
<h5>Adventure Details</h5>
|
|
<ul class="list-unstyled">
|
|
<li><strong>Difficulty:</strong> ${difficultyStars} ${difficultyLabels[difficulty] || 'Not set'}</li>
|
|
<li><strong>Status:</strong> <span class="badge bg-warning">Pending Review</span></li>
|
|
<li><strong>Last Updated:</strong> Today</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Show preview in modal
|
|
document.getElementById('previewContent').innerHTML = previewHTML;
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function submitForm() {
|
|
document.getElementById('editPostForm').submit();
|
|
}
|
|
|
|
// Form submission with AJAX
|
|
document.getElementById('editPostForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(this);
|
|
const submitButton = this.querySelector('button[type="submit"]');
|
|
const originalText = submitButton.innerHTML;
|
|
|
|
// Show loading state
|
|
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
|
|
submitButton.disabled = true;
|
|
|
|
fetch(this.action, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Show success message
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-success alert-dismissible fade show';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-check-circle"></i> ${data.message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
this.insertBefore(alert, this.firstChild);
|
|
|
|
// Redirect after delay
|
|
setTimeout(() => {
|
|
window.location.href = data.redirect_url;
|
|
}, 2000);
|
|
} else {
|
|
// Show error message
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-danger alert-dismissible fade show';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-exclamation-triangle"></i> ${data.error}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
this.insertBefore(alert, this.firstChild);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-danger alert-dismissible fade show';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-exclamation-triangle"></i> An error occurred while updating your post.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
this.insertBefore(alert, this.firstChild);
|
|
})
|
|
.finally(() => {
|
|
// Restore button state
|
|
submitButton.innerHTML = originalText;
|
|
submitButton.disabled = false;
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|