- 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
336 lines
17 KiB
HTML
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 %}
|