Files
moto-adv-website/app/templates/community/index.html
ske087 187254beca feat: Add interactive map functionality with Leaflet.js
- Implemented interactive map card with expand functionality
- Added Leaflet.js integration with OpenStreetMap tiles
- Created expandable map modal (80% screen coverage)
- Fixed cover image display on community page
- Enhanced post detail page with interactive route visualization
- Added proper error handling and fallback content
- Cleaned up JavaScript structure and removed duplicate code
- Updated community index template to use cover images
- Added GPX file processing utilities
- Fixed indentation error in run.py

Map features:
- Country-level positioning (Romania default)
- Zoom controls and interactive navigation
- Test marker with popup functionality
- Expandable full-screen view with X button
- Clean console logging for debugging
- Responsive design with Tailwind CSS styling
2025-07-24 21:36:42 +03:00

336 lines
17 KiB
HTML

{% 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">
{% set cover_image = post.images.filter_by(is_cover=True).first() %}
{% if cover_image %}
<div class="aspect-w-16 aspect-h-9 bg-gray-900">
<img src="{{ cover_image.get_url() }}"
alt="{{ post.title }}"
class="w-full h-48 object-cover">
</div>
{% elif post.images %}
<div class="aspect-w-16 aspect-h-9 bg-gray-900">
<img src="{{ post.images[0].get_url() }}"
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.count() }}
</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.count() }}
</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 %}