Files
moto-adv-website/app/templates/community/new_post.html
ske087 6a0548b880 Final cleanup: Complete Flask motorcycle adventure app
- Removed all Node.js/Next.js dependencies and files
- Cleaned up project structure to contain only Flask application
- Updated .gitignore to exclude Python cache files, virtual environments, and development artifacts
- Complete motorcycle adventure community website with:
  * Interactive Romania map with GPX route plotting
  * Advanced post creation with cover images, sections, highlights
  * User authentication and authorization system
  * Community features with likes and comments
  * Responsive design with blue-purple-teal gradient theme
  * Docker and production deployment configuration
  * SQLite database with proper models and relationships
  * Image and GPX file upload handling
  * Modern UI with improved form layouts and visual feedback

Technical stack:
- Flask 3.0.0 with SQLAlchemy, Flask-Login, Flask-Mail, Flask-WTF
- Jinja2 templates with Tailwind CSS styling
- Leaflet.js for interactive mapping
- PostgreSQL/SQLite database support
- Docker containerization with Nginx reverse proxy
- Gunicorn WSGI server for production

Project is now production-ready Flask application focused on motorcycle adventure sharing in Romania.
2025-07-23 17:20:52 +03:00

596 lines
26 KiB
HTML

{% extends "base.html" %}
{% block title %}Share Your Adventure - Create New Post{% endblock %}
{% block head %}
<style>
.content-section {
border: 2px dashed #d1d5db;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1rem;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.05);
}
.content-section.editing {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.content-section.saved {
border-color: #10b981;
border-style: solid;
background: rgba(16, 185, 129, 0.1);
}
.highlight-input {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: white;
font-weight: 600;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
margin: 0 0.25rem;
}
.highlight-display {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: white;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
margin: 0 0.25rem;
display: inline-block;
}
.image-preview {
position: relative;
display: inline-block;
margin: 0.5rem;
}
.image-preview img {
width: 150px;
height: 100px;
object-fit: cover;
border-radius: 0.5rem;
border: 2px solid #e5e7eb;
}
.image-preview .remove-btn {
position: absolute;
top: -8px;
right: -8px;
background: #ef4444;
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
}
/* Cover upload styles */
.cover-upload-area {
transition: all 0.3s ease;
cursor: pointer;
}
.cover-upload-area:hover {
background: rgba(255, 255, 255, 0.05);
}
/* Dropdown styling */
select option {
background-color: #1f2937;
color: #ffffff;
}
select option:checked {
background-color: #0891b2;
}
/* Section styling */
.content-section {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.section-actions-frame {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.add-section-frame {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1));
border: 1px solid rgba(59, 130, 246, 0.3);
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 py-12">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-white mb-4">
🏍️ Share Your Adventure
</h1>
<p class="text-blue-200 text-lg">
Create a detailed story of your motorcycle journey through Romania
</p>
</div>
<!-- Main Form -->
<form id="postForm" enctype="multipart/form-data" class="space-y-6">
<!-- Basic Information Section -->
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold text-white mb-6">📝 Basic Information</h2>
<!-- Cover Picture -->
<div class="mb-6">
<label for="cover_picture" class="block text-white font-semibold mb-2">Set Cover Picture for the Post</label>
<div class="cover-upload-area border-2 border-dashed border-white/30 rounded-lg p-6 text-center hover:border-white/50 transition-all duration-300">
<input type="file" id="cover_picture" name="cover_picture" accept="image/*" class="hidden">
<div class="cover-upload-content">
<div class="text-4xl mb-2">📸</div>
<p class="text-white/80 mb-2">Click to upload cover image</p>
<p class="text-sm text-white/60">Recommended: 1920x1080 pixels</p>
</div>
<div class="cover-preview hidden">
<img class="cover-preview-image max-h-48 mx-auto rounded-lg" alt="Cover preview">
<button type="button" class="cover-remove-btn mt-2 px-3 py-1 bg-red-500/80 text-white rounded hover:bg-red-600 transition-colors">Remove</button>
</div>
</div>
</div>
<!-- Title -->
<div class="mb-6">
<label for="title" class="block text-white font-semibold mb-2">Adventure Title *</label>
<input type="text" id="title" name="title" required
class="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20
text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-cyan-400
focus:border-transparent transition-all duration-300"
placeholder="Give your adventure a captivating title...">
</div>
<!-- Subtitle -->
<div class="mb-6">
<label for="subtitle" class="block text-white font-semibold mb-2">Subtitle</label>
<input type="text" id="subtitle" name="subtitle"
class="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20
text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-cyan-400
focus:border-transparent transition-all duration-300"
placeholder="A brief description of your adventure">
</div>
<!-- Difficulty Rating -->
<div class="mb-6">
<label for="difficulty" class="block text-white font-semibold mb-2">Route Difficulty *</label>
<select id="difficulty" name="difficulty" required
class="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20
text-white focus:outline-none focus:ring-2 focus:ring-cyan-400
focus:border-transparent transition-all duration-300">
<option value="" class="bg-gray-800 text-gray-300">Select difficulty level...</option>
<option value="1" class="bg-gray-800 text-green-400">🟢 Easy - Beginner friendly roads</option>
<option value="2" class="bg-gray-800 text-yellow-400">🟡 Moderate - Some experience needed</option>
<option value="3" class="bg-gray-800 text-orange-400">🟠 Challenging - Experienced riders</option>
<option value="4" class="bg-gray-800 text-red-400">🔴 Difficult - Advanced skills required</option>
<option value="5" class="bg-gray-800 text-purple-400">🟣 Expert - Only for experts</option>
</select>
</div>
</div>
<!-- Content Sections -->
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold text-white mb-6">📖 Adventure Story</h2>
<div id="content-sections">
<!-- Initial content section will be added by JavaScript -->
</div>
<!-- Add New Section Frame -->
<div class="add-section-frame bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-400/30 rounded-lg p-6 text-center mt-6">
<div class="mb-4">
<h3 class="text-lg font-semibold text-blue-300 mb-2">✨ Expand Your Story</h3>
<p class="text-white/80 text-sm">Add another section with new text, pictures, and highlights to make your adventure more detailed and engaging</p>
</div>
<button type="button" id="add-section-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg transition-all duration-300 transform hover:scale-105 inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Add New Section
</button>
</div>
</div>
<!-- GPX Upload -->
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold text-white mb-6">🗺️ Route File</h2>
<div class="border-2 border-dashed border-white/30 rounded-lg p-8 text-center">
<i class="fas fa-route text-4xl text-blue-300 mb-4"></i>
<div class="text-white font-semibold mb-2">Upload GPX Route File</div>
<div class="text-blue-200 text-sm mb-4">
Share your exact route so others can follow your adventure
</div>
<input type="file" id="gpx-file" name="gpx_file" accept=".gpx" class="hidden">
<label for="gpx-file" class="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer transition">
<i class="fas fa-upload mr-2"></i>
Choose GPX File
</label>
<div id="gpx-filename" class="mt-4 text-green-300 hidden"></div>
</div>
</div>
<!-- Submit -->
<div class="text-center">
<button type="submit" class="bg-gradient-to-r from-orange-500 to-red-600 text-white px-12 py-4 rounded-lg font-bold text-lg hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<i class="fas fa-paper-plane mr-3"></i>
Share Adventure
</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let sectionCounter = 0;
// Cover picture upload
const coverUploadArea = document.querySelector('.cover-upload-area');
const coverInput = document.getElementById('cover_picture');
const coverContent = document.querySelector('.cover-upload-content');
const coverPreview = document.querySelector('.cover-preview');
const coverImage = document.querySelector('.cover-preview-image');
const coverRemoveBtn = document.querySelector('.cover-remove-btn');
coverUploadArea.addEventListener('click', () => coverInput.click());
coverInput.addEventListener('change', function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
coverImage.src = e.target.result;
coverContent.style.display = 'none';
coverPreview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
});
coverRemoveBtn.addEventListener('click', function(e) {
e.stopPropagation();
coverInput.value = '';
coverContent.style.display = 'block';
coverPreview.classList.add('hidden');
});
// GPX file input
document.getElementById('gpx-file').addEventListener('change', function() {
const filename = this.files[0]?.name;
const filenameDiv = document.getElementById('gpx-filename');
if (filename) {
filenameDiv.textContent = `Selected: ${filename}`;
filenameDiv.classList.remove('hidden');
} else {
filenameDiv.classList.add('hidden');
}
});
// Add initial content section
addContentSection();
// Add section button
document.getElementById('add-section-btn').addEventListener('click', addContentSection);
function addContentSection() {
sectionCounter++;
const sectionsContainer = document.getElementById('content-sections');
const section = document.createElement('div');
section.className = 'content-section editing';
section.dataset.sectionId = sectionCounter;
section.innerHTML = `
<div class="section-header mb-4">
<h3 class="text-lg font-semibold text-white">Section ${sectionCounter}</h3>
</div>
<div class="section-content">
<!-- Highlights Input -->
<div class="mb-4">
<label class="block text-white font-semibold mb-2">Key Highlights (1-5 words)</label>
<div class="highlights-container mb-2">
<input type="text" class="highlight-input bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 px-3 py-2 rounded mr-2" placeholder="Highlight 1" maxlength="20">
<input type="text" class="highlight-input bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 px-3 py-2 rounded mr-2" placeholder="Highlight 2" maxlength="20">
<input type="text" class="highlight-input bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 px-3 py-2 rounded mr-2" placeholder="Highlight 3" maxlength="20">
<button type="button" class="add-highlight-btn text-white bg-blue-600 px-3 py-1 rounded ml-2 hover:bg-blue-700 transition">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<!-- Text Content -->
<div class="mb-4">
<label class="block text-white font-semibold mb-2">Section Content</label>
<textarea class="section-text w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:border-transparent"
rows="6" placeholder="Describe this part of your adventure..."></textarea>
</div>
<!-- Image Upload -->
<div class="mb-6">
<label class="block text-white font-semibold mb-2">Photos</label>
<div class="image-upload-area border-2 border-dashed border-white/30 rounded-lg p-4 text-center hover:border-white/50 transition-all duration-300">
<input type="file" class="section-images" multiple accept="image/*" style="display: none;">
<button type="button" class="upload-images-btn bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">
<i class="fas fa-camera mr-2"></i> Add Photos
</button>
<div class="images-preview mt-4"></div>
</div>
</div>
<!-- Section Actions Frame -->
<div class="section-actions-frame bg-white/5 border border-white/20 rounded-lg p-4 mb-4">
<div class="text-center mb-3">
<p class="text-yellow-300 font-medium">💡 Section Actions</p>
<p class="text-white/70 text-sm">Save your content, delete this section, or edit after saving</p>
</div>
<div class="section-actions flex justify-center space-x-3">
<button type="button" class="save-section-btn bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition">
<i class="fas fa-save mr-1"></i> Save
</button>
<button type="button" class="delete-section-btn bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition">
<i class="fas fa-trash mr-1"></i> Delete
</button>
<button type="button" class="edit-section-btn bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition hidden">
<i class="fas fa-edit mr-1"></i> Edit
</button>
</div>
</div>
</div>
<!-- Saved Content Display (hidden initially) -->
<div class="saved-content hidden">
<div class="saved-highlights mb-4"></div>
<div class="saved-text mb-4 text-white"></div>
<div class="saved-images"></div>
</div>
`;
sectionsContainer.appendChild(section);
// Add event listeners for this section
setupSectionEventListeners(section);
}
function setupSectionEventListeners(section) {
const sectionId = section.dataset.sectionId;
// Image upload
const uploadBtn = section.querySelector('.upload-images-btn');
const imageInput = section.querySelector('.section-images');
const imagesPreview = section.querySelector('.images-preview');
uploadBtn.addEventListener('click', () => imageInput.click());
imageInput.addEventListener('change', function() {
const files = Array.from(this.files);
files.forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const imagePreview = document.createElement('div');
imagePreview.className = 'image-preview';
imagePreview.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<div class="remove-btn">&times;</div>
`;
imagePreview.querySelector('.remove-btn').addEventListener('click', function() {
imagePreview.remove();
});
imagesPreview.appendChild(imagePreview);
};
reader.readAsDataURL(file);
}
});
});
// Add highlight button
section.querySelector('.add-highlight-btn').addEventListener('click', function() {
const highlightsContainer = section.querySelector('.highlights-container');
const newHighlight = document.createElement('input');
newHighlight.type = 'text';
newHighlight.className = 'highlight-input';
newHighlight.placeholder = 'New highlight';
newHighlight.maxLength = 20;
highlightsContainer.insertBefore(newHighlight, this);
});
// Save section
section.querySelector('.save-section-btn').addEventListener('click', function() {
saveSection(section);
});
// Delete section
section.querySelector('.delete-section-btn').addEventListener('click', function() {
if (confirm('Are you sure you want to delete this section?')) {
section.remove();
}
});
// Edit section
section.querySelector('.edit-section-btn').addEventListener('click', function() {
editSection(section);
});
}
function saveSection(section) {
const highlights = Array.from(section.querySelectorAll('.highlight-input'))
.map(input => input.value.trim())
.filter(value => value);
const text = section.querySelector('.section-text').value.trim();
const images = section.querySelectorAll('.images-preview img');
if (!text && highlights.length === 0 && images.length === 0) {
alert('Please add some content to this section before saving.');
return;
}
// Show saved content
const savedContent = section.querySelector('.saved-content');
const savedHighlights = section.querySelector('.saved-highlights');
const savedText = section.querySelector('.saved-text');
const savedImages = section.querySelector('.saved-images');
// Display highlights
savedHighlights.innerHTML = highlights.map(h =>
`<span class="highlight-display bg-cyan-600/20 text-cyan-300 px-2 py-1 rounded mr-2">${h}</span>`
).join('');
// Display text
savedText.innerHTML = text.replace(/\n/g, '<br>');
// Display images
savedImages.innerHTML = '';
images.forEach(img => {
const imgCopy = img.cloneNode();
imgCopy.className = 'w-32 h-24 object-cover rounded m-1';
savedImages.appendChild(imgCopy);
});
// Hide editing interface and show saved content
section.querySelector('.section-content').style.display = 'none';
savedContent.classList.remove('hidden');
// Update button visibility in actions frame
section.querySelector('.save-section-btn').style.display = 'none';
section.querySelector('.delete-section-btn').style.display = 'none';
section.querySelector('.edit-section-btn').style.display = 'inline-flex';
// Update section appearance
section.classList.remove('editing');
section.classList.add('saved');
// Update header
section.querySelector('.section-header h3').textContent = `Section ${section.dataset.sectionId}`;
// Update actions frame text
const actionsFrame = section.querySelector('.section-actions-frame');
actionsFrame.querySelector('p:first-child').innerHTML = '✅ <span class="text-green-300">Section Saved</span>';
actionsFrame.querySelector('p:last-child').textContent = 'Your content has been saved. You can edit it again if needed.';
}
function editSection(section) {
// Show editing interface and hide saved content
section.querySelector('.section-content').style.display = 'block';
section.querySelector('.saved-content').classList.add('hidden');
// Update button visibility in actions frame
section.querySelector('.save-section-btn').style.display = 'inline-flex';
section.querySelector('.delete-section-btn').style.display = 'inline-flex';
section.querySelector('.edit-section-btn').style.display = 'none';
// Update section appearance
section.classList.remove('saved');
section.classList.add('editing');
// Update header
section.querySelector('.section-header h3').textContent = `Section ${section.dataset.sectionId}`;
// Restore actions frame text
const actionsFrame = section.querySelector('.section-actions-frame');
actionsFrame.querySelector('p:first-child').innerHTML = '💡 <span class="text-yellow-300">Section Actions</span>';
actionsFrame.querySelector('p:last-child').textContent = 'Save your content, delete this section, or edit after saving';
}
// Form submission
document.getElementById('adventure-form').addEventListener('submit', function(e) {
e.preventDefault();
// Validate required fields
const title = document.getElementById('title').value.trim();
const difficulty = document.getElementById('difficulty').value;
if (!title) {
alert('Please enter an adventure title.');
return;
}
if (!difficulty) {
alert('Please select a difficulty level.');
return;
}
// Check if at least one section is saved
const savedSections = document.querySelectorAll('.content-section.saved');
if (savedSections.length === 0) {
alert('Please save at least one content section.');
return;
}
// Collect all form data
const formData = new FormData();
formData.append('title', title);
formData.append('subtitle', document.getElementById('subtitle').value.trim());
formData.append('difficulty', difficulty);
// Collect content from saved sections
let fullContent = '';
savedSections.forEach((section, index) => {
const highlights = Array.from(section.querySelectorAll('.saved-highlights .highlight-display'))
.map(span => span.textContent);
const text = section.querySelector('.saved-text').textContent;
if (highlights.length > 0) {
fullContent += highlights.map(h => `**${h}**`).join(' ') + '\n\n';
}
if (text) {
fullContent += text + '\n\n';
}
});
formData.append('content', fullContent);
// Add GPX file if selected
const gpxFile = document.getElementById('gpx-file').files[0];
if (gpxFile) {
formData.append('gpx_file', gpxFile);
}
// Add images (simplified - in a real implementation, you'd handle this properly)
const allImages = document.querySelectorAll('.saved-images img');
// Note: This is a simplified version. In a real implementation,
// you'd need to properly handle the image files
// Submit form
fetch('{{ url_for("community.new_post") }}', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect_url;
} else {
alert('Error creating post: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while creating your post.');
});
});
});
</script>
{% endblock %}