From 6a0548b88072447ee08f669a52f7badb9db2abef Mon Sep 17 00:00:00 2001
From: ske087
Date: Wed, 23 Jul 2025 17:20:52 +0300
Subject: [PATCH] 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.
---
.env.example | 0
.gitignore | 121 ++++++
app/__init__.py | 15 +-
app/extensions.py | 10 +
app/models.py | 7 +-
app/routes/auth.py | 5 +-
app/routes/community.py | 153 +++++--
app/templates/base.html | 6 +-
app/templates/community/index.html | 328 ++++++++++++++
app/templates/community/new_post.html | 595 ++++++++++++++++++++++++++
app/templates/index.html | 14 +-
config.py | 4 +-
12 files changed, 1186 insertions(+), 72 deletions(-)
create mode 100644 .env.example
create mode 100644 app/extensions.py
create mode 100644 app/templates/community/index.html
create mode 100644 app/templates/community/new_post.html
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e69de29
diff --git a/.gitignore b/.gitignore
index e69de29..8578775 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,121 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Application specific
+uploads/
+*.db
+*.sqlite
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
index bda8bb1..4e479e2 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,16 +1,8 @@
from flask import Flask
-from flask_sqlalchemy import SQLAlchemy
-from flask_migrate import Migrate
-from flask_login import LoginManager
-from flask_mail import Mail
+from app.extensions import db, migrate, login_manager, mail
from config import config
import os
-db = SQLAlchemy()
-migrate = Migrate()
-login_manager = LoginManager()
-mail = Mail()
-
def create_app(config_name=None):
app = Flask(__name__)
@@ -53,8 +45,3 @@ def create_app(config_name=None):
os.makedirs(os.path.join(upload_dir, 'gpx'), exist_ok=True)
return app
-
-@login_manager.user_loader
-def load_user(user_id):
- from app.models import User
- return User.query.get(int(user_id))
diff --git a/app/extensions.py b/app/extensions.py
new file mode 100644
index 0000000..894167d
--- /dev/null
+++ b/app/extensions.py
@@ -0,0 +1,10 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager
+from flask_mail import Mail
+
+# Initialize extensions
+db = SQLAlchemy()
+migrate = Migrate()
+login_manager = LoginManager()
+mail = Mail()
diff --git a/app/models.py b/app/models.py
index 7ed355b..0c52891 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,9 +1,7 @@
from datetime import datetime
-from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
-
-db = SQLAlchemy()
+from app.extensions import db
class User(UserMixin, db.Model):
__tablename__ = 'users'
@@ -70,7 +68,8 @@ class PostImage(db.Model):
original_name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
size = db.Column(db.Integer, nullable=False)
- mime_type = db.Column(db.String(100), nullable=False)
+ mime_type = db.Column(db.String(100), default='image/jpeg', nullable=False)
+ is_cover = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Foreign Keys
diff --git a/app/routes/auth.py b/app/routes/auth.py
index 0daa5ed..f981e1f 100644
--- a/app/routes/auth.py
+++ b/app/routes/auth.py
@@ -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')
diff --git a/app/routes/community.py b/app/routes/community.py
index 27c3dfe..0ad9b69 100644
--- a/app/routes/community.py
+++ b/app/routes/community.py
@@ -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/')
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//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)
diff --git a/app/templates/base.html b/app/templates/base.html
index 2464d4b..7c29ca2 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -22,12 +22,12 @@
@@ -40,6 +40,12 @@
Join our passionate community of motorcycle enthusiasts as we explore the most spectacular
routes Romania has to offer. From winding mountain passes to scenic coastal roads.
+
diff --git a/config.py b/config.py
index b700107..fb215eb 100644
--- a/config.py
+++ b/config.py
@@ -5,7 +5,7 @@ load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost/moto_adventure'
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///moto_adventure.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# File Upload Configuration
@@ -31,7 +31,7 @@ class Config:
class DevelopmentConfig(Config):
DEBUG = True
- SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'postgresql://postgres:password@localhost/moto_adventure_dev'
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///moto_adventure_dev.db'
class ProductionConfig(Config):
DEBUG = False