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

0
.env.example Normal file
View File

121
.gitignore vendored
View File

@@ -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

View File

@@ -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))

10
app/extensions.py Normal file
View File

@@ -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()

View File

@@ -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

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)

View File

@@ -22,12 +22,12 @@
</a>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="{{ url_for('main.index') }}#about" class="text-white hover:text-blue-200 transition">About</a>
<a href="{{ url_for('main.index') }}" class="text-white hover:text-blue-200 transition">Landing</a>
<a href="{{ url_for('community.index') }}" class="text-white hover:text-teal-200 transition font-semibold">🏍️ Adventures</a>
<a href="{{ url_for('main.index') }}#accommodation" class="text-white hover:text-purple-200 transition">Accommodation</a>
<a href="{{ url_for('community.index') }}" class="text-white hover:text-teal-200 transition">Stories & Tracks</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}" class="bg-gradient-to-r from-green-500 to-emerald-500 text-white px-4 py-2 rounded-lg hover:from-green-400 hover:to-emerald-400 transition transform hover:scale-105">
<i class="fas fa-plus mr-2"></i>New Post
<i class="fas fa-plus mr-2"></i>Share Adventure
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="text-white hover:text-yellow-200 transition">

View File

@@ -0,0 +1,328 @@
{% extends "base.html" %}
{% block title %}Motorcycle Adventures Romania{% endblock %}
{% block head %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
.map-container {
height: 500px;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.route-popup {
font-family: inherit;
}
.route-popup .popup-title {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.route-popup .popup-author {
color: #6b7280;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.route-popup .popup-link {
background: linear-gradient(135deg, #f97316, #dc2626);
color: white;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
display: inline-block;
transition: all 0.2s;
}
.route-popup .popup-link:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900">
<!-- Header Section -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 bg-black/20"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="text-center mb-8">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
🏍️ Motorcycle Adventures Romania
</h1>
<p class="text-lg text-blue-100 max-w-3xl mx-auto">
Discover epic motorcycle routes, share your adventures, and connect with fellow riders across Romania's stunning landscapes.
</p>
</div>
</div>
</div>
<!-- Interactive Map Section -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-12">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">🗺️ Interactive Route Map</h2>
<div class="text-sm text-blue-200">
<span id="route-count">{{ posts_with_routes|length }}</span> routes discovered
</div>
</div>
<div id="romania-map" class="map-container"></div>
<p class="text-blue-200 text-sm mt-4 text-center">
Click on any route to view the adventure story • Routes are updated live as new trips are shared
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-8">
<div class="flex flex-wrap justify-center gap-4">
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Share Your Adventure
</a>
<a href="{{ url_for('auth.logout') }}"
class="inline-flex items-center px-6 py-3 border border-white/30 text-base font-medium rounded-lg text-white hover:bg-white/10 transition-all duration-200">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
</svg>
Logout
</a>
{% else %}
<a href="{{ url_for('auth.register') }}"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
</svg>
Join Community
</a>
<a href="{{ url_for('auth.login') }}"
class="inline-flex items-center px-6 py-3 border border-white/30 text-base font-medium rounded-lg text-white hover:bg-white/10 transition-all duration-200">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
Login
</a>
{% endif %}
</div>
</div>
<!-- Latest Adventures Grid -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex justify-between items-center mb-8">
<h2 class="text-3xl font-bold text-white">Latest Adventures</h2>
<div class="text-blue-200 text-sm">
Showing {{ posts.items|length }} of {{ posts.total }} stories
</div>
</div>
{% if posts.items %}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for post in posts.items %}
<article class="bg-white/10 backdrop-blur-sm rounded-xl overflow-hidden shadow-xl hover:shadow-2xl transition-all duration-300 border border-white/20 hover:border-white/40 transform hover:-translate-y-1">
{% if post.images %}
<div class="aspect-w-16 aspect-h-9 bg-gray-900">
<img src="{{ url_for('static', filename='uploads/images/' + post.images[0].filename) }}"
alt="{{ post.title }}"
class="w-full h-48 object-cover">
</div>
{% endif %}
<div class="p-5">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-gradient-to-r from-orange-500 to-red-600 rounded-full flex items-center justify-center">
<span class="text-white font-bold text-xs">{{ post.author.nickname[0].upper() }}</span>
</div>
</div>
<div class="ml-2">
<p class="text-sm font-medium text-white">{{ post.author.nickname }}</p>
<p class="text-xs text-blue-200">{{ post.created_at.strftime('%b %d, %Y') }}</p>
</div>
</div>
<h3 class="text-lg font-bold text-white mb-2 line-clamp-2">
<a href="{{ url_for('community.post_detail', id=post.id) }}"
class="hover:text-orange-400 transition-colors">
{{ post.title }}
</a>
</h3>
{% if post.subtitle %}
<p class="text-sm text-blue-200 mb-2 line-clamp-1">{{ post.subtitle }}</p>
{% endif %}
<p class="text-blue-100 text-sm mb-4 line-clamp-2">
{{ post.content[:120] }}{% if post.content|length > 120 %}...{% endif %}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 text-xs text-blue-200">
{% if post.gpx_files %}
<span class="flex items-center bg-green-500/20 px-2 py-1 rounded-full">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
GPX
</span>
{% endif %}
<span class="flex items-center">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
{{ post.comments|length }}
</span>
<span class="flex items-center">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path>
</svg>
{{ post.likes|length }}
</span>
</div>
<a href="{{ url_for('community.post_detail', id=post.id) }}"
class="text-orange-400 hover:text-orange-300 font-medium text-xs transition-colors">
Read More →
</a>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination -->
{% if posts.pages > 1 %}
<div class="mt-12 flex justify-center">
<nav class="flex items-center space-x-2">
{% if posts.has_prev %}
<a href="{{ url_for('community.index', page=posts.prev_num) }}"
class="px-4 py-2 text-white bg-white/10 backdrop-blur-sm rounded-lg hover:bg-white/20 transition-colors">
← Previous
</a>
{% endif %}
{% for page_num in posts.iter_pages() %}
{% if page_num %}
{% if page_num != posts.page %}
<a href="{{ url_for('community.index', page=page_num) }}"
class="px-3 py-2 text-white bg-white/10 backdrop-blur-sm rounded-lg hover:bg-white/20 transition-colors">
{{ page_num }}
</a>
{% else %}
<span class="px-3 py-2 text-white bg-orange-500 rounded-lg">{{ page_num }}</span>
{% endif %}
{% else %}
<span class="px-3 py-2 text-blue-200"></span>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<a href="{{ url_for('community.index', page=posts.next_num) }}"
class="px-4 py-2 text-white bg-white/10 backdrop-blur-sm rounded-lg hover:bg-white/20 transition-colors">
Next →
</a>
{% endif %}
</nav>
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="text-center py-16">
<div class="max-w-md mx-auto">
<svg class="w-16 h-16 text-blue-300 mx-auto mb-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
</svg>
<h3 class="text-2xl font-bold text-white mb-4">No Adventures Yet</h3>
<p class="text-blue-200 mb-6">
Be the first to share your motorcycle adventure and map your route across Romania!
</p>
{% if current_user.is_authenticated %}
<a href="{{ url_for('community.new_post') }}"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200">
Share Your First Adventure
</a>
{% else %}
<a href="{{ url_for('auth.register') }}"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200">
Join to Share Adventures
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize map centered on Romania
var map = L.map('romania-map').setView([45.9432, 24.9668], 7);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Custom route colors
var routeColors = ['#f97316', '#dc2626', '#059669', '#7c3aed', '#db2777', '#2563eb'];
var colorIndex = 0;
// Load and display routes
fetch('{{ url_for("community.api_routes") }}')
.then(response => response.json())
.then(routes => {
routes.forEach(route => {
if (route.coordinates && route.coordinates.length > 0) {
// Create polyline for the route
var routeLine = L.polyline(route.coordinates, {
color: routeColors[colorIndex % routeColors.length],
weight: 4,
opacity: 0.8
}).addTo(map);
// Create popup content
var popupContent = `
<div class="route-popup">
<div class="popup-title">${route.title}</div>
<div class="popup-author">by ${route.author}</div>
<a href="${route.url}" class="popup-link">View Adventure</a>
</div>
`;
// Add popup to route
routeLine.bindPopup(popupContent);
// Add click event to highlight route
routeLine.on('click', function(e) {
this.setStyle({
weight: 6,
opacity: 1
});
setTimeout(() => {
this.setStyle({
weight: 4,
opacity: 0.8
});
}, 2000);
});
colorIndex++;
}
});
// Update route count
document.getElementById('route-count').textContent = routes.length;
})
.catch(error => {
console.error('Error loading routes:', error);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,595 @@
{% 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 %}

View File

@@ -18,11 +18,11 @@
and find the perfect accommodation for your next adventure.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="#about" class="bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold text-lg hover:bg-gray-100 transition transform hover:scale-105 shadow-lg">
<i class="fas fa-compass mr-2"></i>Start Exploring
<a href="{{ url_for('community.index') }}" class="bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold text-lg hover:bg-gray-100 transition transform hover:scale-105 shadow-lg">
<i class="fas fa-map-marked-alt mr-2"></i>Explore Adventures
</a>
<a href="{{ url_for('community.index') }}" class="border-2 border-white text-white px-8 py-4 rounded-lg font-semibold text-lg hover:bg-white hover:text-purple-600 transition transform hover:scale-105">
<i class="fas fa-users mr-2"></i>Join Community
<a href="#accommodation" class="border-2 border-white text-white px-8 py-4 rounded-lg font-semibold text-lg hover:bg-white hover:text-purple-600 transition transform hover:scale-105">
<i class="fas fa-bed mr-2"></i>Find Accommodation
</a>
</div>
</div>
@@ -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.
</p>
<div class="mt-8">
<a href="{{ url_for('community.index') }}" class="inline-flex items-center px-8 py-4 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-700 hover:to-purple-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<i class="fas fa-map-marked-alt mr-3"></i>
Explore Interactive Route Map
</a>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">

View File

@@ -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