Migrate from Next.js to Flask: Complete motorcycle adventure community website

- Replace Next.js/React implementation with Python Flask
- Add colorful blue-purple-teal gradient theme replacing red design
- Integrate logo and Transalpina panoramic background image
- Implement complete authentication system with Flask-Login
- Add community features for stories and tracks sharing
- Create responsive design with Tailwind CSS
- Add error handling with custom 404/500 pages
- Include Docker deployment configuration
- Add favicon support and proper SEO structure
- Update content for Pensiune BuonGusto accommodation
- Remove deprecated Next.js files and dependencies

Features:
 Landing page with hero section and featured content
 User registration and login system
 Community section for adventure sharing
 Admin panel for content management
 Responsive mobile-first design
 Docker containerization with PostgreSQL
 Email integration with Flask-Mail
 Form validation with WTForms
 SQLAlchemy database models
 Error pages and favicon handling
This commit is contained in:
ske087
2025-07-23 14:42:40 +03:00
parent 282cd0dfcb
commit fc463dc69a
45 changed files with 1572 additions and 11044 deletions

106
app/routes/auth.py Normal file
View File

@@ -0,0 +1,106 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash
from app.models import User, db
from app.forms import LoginForm, RegisterForm, ForgotPasswordForm
import re
auth = Blueprint('auth', __name__)
@auth.route('/login', methods=['GET', 'POST'])
def login():
"""User login page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('community.index')
flash(f'Welcome back, {user.nickname}!', 'success')
return redirect(next_page)
else:
flash('Invalid email or password.', 'error')
return render_template('auth/login.html', form=form)
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""User registration page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegisterForm()
if form.validate_on_submit():
# Check if user already exists
if User.query.filter_by(email=form.email.data).first():
flash('Email address already registered.', 'error')
return render_template('auth/register.html', form=form)
if User.query.filter_by(nickname=form.nickname.data).first():
flash('Nickname already taken.', 'error')
return render_template('auth/register.html', form=form)
# Validate password strength
if not is_valid_password(form.password.data):
flash('Password must be at least 8 characters long and contain at least one letter and one number.', 'error')
return render_template('auth/register.html', form=form)
# Create new user
user = User(
nickname=form.nickname.data,
email=form.email.data
)
user.set_password(form.password.data)
try:
db.session.add(user)
db.session.commit()
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
db.session.rollback()
flash('An error occurred during registration. Please try again.', 'error')
return render_template('auth/register.html', form=form)
@auth.route('/logout')
@login_required
def logout():
"""User logout"""
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.index'))
@auth.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
"""Forgot password page"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ForgotPasswordForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
# TODO: Implement email sending for password reset
flash('If an account with that email exists, we\'ve sent password reset instructions.', 'info')
else:
flash('If an account with that email exists, we\'ve sent password reset instructions.', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/forgot_password.html', form=form)
def is_valid_password(password):
"""Validate password strength"""
if len(password) < 8:
return False
if not re.search(r'[A-Za-z]', password):
return False
if not re.search(r'\d', password):
return False
return True

195
app/routes/community.py Normal file
View File

@@ -0,0 +1,195 @@
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.forms import PostForm, CommentForm
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import os
import uuid
from datetime import datetime
from PIL import Image
import gpxpy
community = Blueprint('community', __name__)
@community.route('/')
def index():
"""Community posts listing page"""
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
)
return render_template('community/index.html', posts=posts)
@community.route('/post/<int:id>')
def post_detail(id):
"""Individual post detail page"""
post = Post.query.get_or_404(id)
if not post.published and (not current_user.is_authenticated or
(current_user.id != post.author_id and not current_user.is_admin)):
flash('Post not found.', 'error')
return redirect(url_for('community.index'))
form = CommentForm()
comments = Comment.query.filter_by(post_id=id).order_by(Comment.created_at.asc()).all()
return render_template('community/post_detail.html', post=post, form=form, comments=comments)
@community.route('/new-post', methods=['GET', 'POST'])
@login_required
def new_post():
"""Create new post page"""
form = PostForm()
if form.validate_on_submit():
try:
# 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,
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 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)
db.session.commit()
flash('Your adventure has been shared!', 'success')
return redirect(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 render_template('community/new_post.html', form=form)
@community.route('/post/<int:id>/comment', methods=['POST'])
@login_required
def add_comment(id):
"""Add comment to post"""
post = Post.query.get_or_404(id)
form = CommentForm()
if form.validate_on_submit():
comment = Comment(
content=form.content.data,
author_id=current_user.id,
post_id=post.id
)
db.session.add(comment)
db.session.commit()
flash('Your comment has been added.', 'success')
return redirect(url_for('community.post_detail', id=id))
@community.route('/post/<int:id>/like', methods=['POST'])
@login_required
def toggle_like(id):
"""Toggle like on post"""
post = Post.query.get_or_404(id)
existing_like = Like.query.filter_by(user_id=current_user.id, post_id=post.id).first()
if existing_like:
db.session.delete(existing_like)
liked = False
else:
like = Like(user_id=current_user.id, post_id=post.id)
db.session.add(like)
liked = True
db.session.commit()
return jsonify({'liked': liked, 'count': post.get_like_count()})
def save_image(image_file, post_id):
"""Save uploaded image file"""
try:
# Create upload directory
upload_dir = os.path.join(current_app.instance_path, 'uploads', 'images')
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
filename = secure_filename(f"{uuid.uuid4().hex}_{image_file.filename}")
filepath = os.path.join(upload_dir, filename)
# Save and resize image
image = Image.open(image_file)
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
# Resize if too large
max_size = (1920, 1080)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
image.save(filepath, 'JPEG', quality=85, optimize=True)
file_size = os.path.getsize(filepath)
return {
'success': True,
'filename': filename,
'size': file_size
}
except Exception as e:
current_app.logger.error(f'Error saving image: {str(e)}')
return {'success': False, 'error': str(e)}
def save_gpx_file(gpx_file, post_id):
"""Save uploaded GPX file"""
try:
# Create upload directory
upload_dir = os.path.join(current_app.instance_path, 'uploads', 'gpx')
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
filename = secure_filename(f"{uuid.uuid4().hex}_{gpx_file.filename}")
filepath = os.path.join(upload_dir, filename)
# Validate GPX file
gpx_content = gpx_file.read()
gpx = gpxpy.parse(gpx_content.decode('utf-8'))
# Save file
with open(filepath, 'wb') as f:
f.write(gpx_content)
file_size = len(gpx_content)
return {
'success': True,
'filename': filename,
'size': file_size
}
except Exception as e:
current_app.logger.error(f'Error saving GPX file: {str(e)}')
return {'success': False, 'error': str(e)}

39
app/routes/main.py Normal file
View File

@@ -0,0 +1,39 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from app.models import Post, PostImage, GPXFile, User, db
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge
import os
import uuid
from datetime import datetime
main = Blueprint('main', __name__)
@main.route('/')
def index():
"""Landing page with about, accommodation, and community sections"""
return render_template('index.html')
@main.route('/favicon.ico')
def favicon():
"""Serve favicon"""
return redirect(url_for('static', filename='favicon.ico'))
@main.route('/health')
def health_check():
"""Health check endpoint"""
return {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat()}
@main.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@main.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500
@main.app_errorhandler(RequestEntityTooLarge)
def file_too_large(error):
flash('File is too large. Maximum file size is 16MB.', 'error')
return redirect(request.url)