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.
This commit is contained in:
ske087
2025-07-23 17:20:52 +03:00
parent fc463dc69a
commit 6a0548b880
12 changed files with 1186 additions and 72 deletions

View File

@@ -61,8 +61,9 @@ def register():
try:
db.session.add(user)
db.session.commit()
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
login_user(user)
flash('Registration successful! Welcome to the community!', 'success')
return redirect(url_for('community.index'))
except Exception as e:
db.session.rollback()
flash('An error occurred during registration. Please try again.', 'error')

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
from flask_login import login_required, current_user
from app.models import Post, PostImage, GPXFile, User, Comment, Like, db
from app.models import Post, PostImage, GPXFile, User, Comment, Like
from app.extensions import db
from app.forms import PostForm, CommentForm
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
@@ -14,12 +15,16 @@ community = Blueprint('community', __name__)
@community.route('/')
def index():
"""Community posts listing page"""
"""Community main page with map and posts"""
page = request.args.get('page', 1, type=int)
posts = Post.query.filter_by(published=True).order_by(Post.created_at.desc()).paginate(
page=page, per_page=10, error_out=False
page=page, per_page=12, error_out=False
)
return render_template('community/index.html', posts=posts)
# Get posts with GPX files for map display
posts_with_routes = Post.query.filter_by(published=True).join(GPXFile).all()
return render_template('community/index.html', posts=posts, posts_with_routes=posts_with_routes)
@community.route('/post/<int:id>')
def post_detail(id):
@@ -39,61 +44,81 @@ def post_detail(id):
@login_required
def new_post():
"""Create new post page"""
form = PostForm()
if form.validate_on_submit():
if request.method == 'POST':
try:
# Get form data
title = request.form.get('title', '').strip()
subtitle = request.form.get('subtitle', '').strip()
content = request.form.get('content', '').strip()
difficulty = request.form.get('difficulty', type=int)
# Validation
if not title:
return jsonify({'success': False, 'error': 'Title is required'})
if not difficulty or difficulty < 1 or difficulty > 5:
return jsonify({'success': False, 'error': 'Valid difficulty level is required'})
if not content:
return jsonify({'success': False, 'error': 'Content is required'})
# Create post
post = Post(
title=form.title.data,
subtitle=form.subtitle.data,
content=form.content.data,
difficulty=int(form.difficulty.data),
published=form.published.data,
title=title,
subtitle=subtitle,
content=content,
difficulty=difficulty,
published=True, # Auto-publish for now
author_id=current_user.id
)
db.session.add(post)
db.session.flush() # Get the post ID
# Handle image uploads
if form.images.data and form.images.data.filename:
images = request.files.getlist('images')
for image_file in images:
if image_file and image_file.filename:
result = save_image(image_file, post.id)
if result['success']:
post_image = PostImage(
filename=result['filename'],
original_name=image_file.filename,
size=result['size'],
mime_type=image_file.content_type,
post_id=post.id
)
db.session.add(post_image)
# Handle cover picture upload
if 'cover_picture' in request.files:
cover_file = request.files['cover_picture']
if cover_file and cover_file.filename:
result = save_image(cover_file, post.id)
if result['success']:
# Save as cover image
cover_image = PostImage(
filename=result['filename'],
original_name=cover_file.filename,
size=result['size'],
mime_type=result['mime_type'],
post_id=post.id,
is_cover=True
)
db.session.add(cover_image)
# Handle GPX file upload
if form.gpx_file.data and form.gpx_file.data.filename:
result = save_gpx_file(form.gpx_file.data, post.id)
if result['success']:
gpx_file = GPXFile(
filename=result['filename'],
original_name=form.gpx_file.data.filename,
size=result['size'],
post_id=post.id
)
db.session.add(gpx_file)
if 'gpx_file' in request.files:
gpx_file = request.files['gpx_file']
if gpx_file and gpx_file.filename:
result = save_gpx_file(gpx_file, post.id)
if result['success']:
gpx_file_record = GPXFile(
filename=result['filename'],
original_name=gpx_file.filename,
size=result['size'],
post_id=post.id
)
db.session.add(gpx_file_record)
db.session.commit()
flash('Your adventure has been shared!', 'success')
return redirect(url_for('community.post_detail', id=post.id))
return jsonify({
'success': True,
'message': 'Adventure shared successfully!',
'redirect_url': url_for('community.post_detail', id=post.id)
})
except Exception as e:
db.session.rollback()
flash('An error occurred while creating your post. Please try again.', 'error')
current_app.logger.error(f'Error creating post: {str(e)}')
return jsonify({'success': False, 'error': 'An error occurred while creating your post'})
return render_template('community/new_post.html', form=form)
# GET request - show the form
return render_template('community/new_post.html')
@community.route('/post/<int:id>/comment', methods=['POST'])
@login_required
@@ -158,7 +183,8 @@ def save_image(image_file, post_id):
return {
'success': True,
'filename': filename,
'size': file_size
'size': file_size,
'mime_type': 'image/jpeg'
}
except Exception as e:
current_app.logger.error(f'Error saving image: {str(e)}')
@@ -192,4 +218,45 @@ def save_gpx_file(gpx_file, post_id):
}
except Exception as e:
current_app.logger.error(f'Error saving GPX file: {str(e)}')
return {'success': False, 'error': str(e)}
return {
'success': False,
'error': str(e)
}
@community.route('/api/routes')
def api_routes():
"""API endpoint to get all routes for map display"""
posts_with_routes = Post.query.filter_by(published=True).join(GPXFile).all()
routes_data = []
for post in posts_with_routes:
for gpx_file in post.gpx_files:
try:
# Read and parse GPX file
gpx_path = os.path.join(current_app.instance_path, 'uploads', 'gpx', gpx_file.filename)
if os.path.exists(gpx_path):
with open(gpx_path, 'r') as f:
gpx_content = f.read()
gpx = gpxpy.parse(gpx_content)
# Extract coordinates
coordinates = []
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
coordinates.append([point.latitude, point.longitude])
if coordinates:
routes_data.append({
'id': post.id,
'title': post.title,
'author': post.author.nickname,
'coordinates': coordinates,
'url': url_for('community.post_detail', id=post.id)
})
except Exception as e:
current_app.logger.error(f'Error processing GPX file {gpx_file.filename}: {str(e)}')
continue
return jsonify(routes_data)