Fix: Clean up map_iframe_single.html, remove debug overlay, ensure clean map rendering.
This commit is contained in:
@@ -3,42 +3,116 @@
|
||||
{% 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;
|
||||
/* 3-row grid card layout */
|
||||
.map-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 800px;
|
||||
background: rgba(255,255,255,0.07);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
|
||||
border: 2px solid #10b981;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.route-popup {
|
||||
font-family: inherit;
|
||||
.map-card-square {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: 0 auto;
|
||||
background: rgba(255,255,255,0.07);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
|
||||
border: 2px solid #10b981;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.route-popup .popup-title {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
.map-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem 0.5rem 1.25rem;
|
||||
}
|
||||
.route-popup .popup-author {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
.map-card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
.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;
|
||||
.map-card-count {
|
||||
font-size: 0.95rem;
|
||||
color: #a5b4fc;
|
||||
text-align: right;
|
||||
}
|
||||
.route-popup .popup-link:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
.map-card-content {
|
||||
flex: 1 1 0%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.map-iframe-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f0f0f0;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
|
||||
border: 2px solid #10b981;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
border: none;
|
||||
display: block;
|
||||
border-radius: 1rem;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
/* Loading state for iframe */
|
||||
.iframe-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 200px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.iframe-loading.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.map-card-footer {
|
||||
padding: 0.5rem 1.25rem 0.75rem 1.25rem;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.map-iframe-container, .map-iframe {
|
||||
height: 40vh;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -48,29 +122,44 @@
|
||||
<!-- 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">
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-2 pb-2">
|
||||
<div class="text-center mb-2">
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-1">
|
||||
🏍️ Motorcycle Adventures Romania
|
||||
</h1>
|
||||
<p class="text-lg text-blue-100 max-w-3xl mx-auto">
|
||||
<p class="text-base text-blue-100 max-w-2xl 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 -->
|
||||
<!-- Interactive Map Section (Wide Rectangular Card) -->
|
||||
<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="w-11/12 md:w-10/12 lg:w-5/6 xl:w-11/12 mx-auto bg-white/10 backdrop-blur-sm rounded-2xl p-4 border border-white/20 flex flex-col" style="">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="text-xl 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">
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="w-full aspect-[10/7] rounded-xl overflow-hidden border-2 border-emerald-500 bg-gray-100 relative">
|
||||
<!-- Loading indicator -->
|
||||
<div id="iframe-loading" class="absolute inset-0 flex flex-col items-center justify-center bg-white/80 z-10">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-orange-600 mb-2"></div>
|
||||
<span class="text-gray-700 text-sm">Loading interactive map...</span>
|
||||
</div>
|
||||
<iframe
|
||||
id="map-iframe"
|
||||
src="{{ url_for('static', filename='map_iframe.html') }}"
|
||||
class="w-full h-full border-0 rounded-xl bg-gray-100 aspect-[10/7]"
|
||||
title="Adventure Routes Map"
|
||||
onload="hideIframeLoading()">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-blue-200 text-xs mt-2 text-center">
|
||||
Click on any route to view the adventure story • Routes are updated live as new trips are shared
|
||||
</p>
|
||||
</div>
|
||||
@@ -268,68 +357,24 @@
|
||||
</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);
|
||||
});
|
||||
});
|
||||
// Function to hide iframe loading indicator
|
||||
function hideIframeLoading() {
|
||||
const loadingEl = document.getElementById('iframe-loading');
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
loadingEl.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to hide loading after 5 seconds if iframe doesn't trigger onload
|
||||
setTimeout(() => {
|
||||
const loadingEl = document.getElementById('iframe-loading');
|
||||
if (loadingEl && !loadingEl.classList.contains('hidden')) {
|
||||
console.log('Iframe loading timeout, hiding loading indicator');
|
||||
hideIframeLoading();
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -419,6 +419,26 @@
|
||||
<div class="space-y-8">
|
||||
<!-- GPS Map and Route Information -->
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
<!-- Map Card (single route, styled like Adventure Info) -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden mb-8">
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-map-marked-alt text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Map</h2>
|
||||
<p class="text-blue-100">Full trip view</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-0" style="height:400px;overflow:hidden;">
|
||||
<iframe
|
||||
id="route-map-iframe"
|
||||
src="{{ url_for('static', filename='map_iframe_single.html') }}?route_id={{ post.gpx_files[0].id }}"
|
||||
width="100%" height="400" style="border:0; border-radius:1rem; display:block; background:#f8f9fa;"
|
||||
allowfullscreen loading="lazy" referrerpolicy="no-referrer-when-downgrade">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-orange-600 to-red-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
@@ -491,44 +511,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Interactive Map Card -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-teal-600 to-cyan-600 p-6">
|
||||
<div class="flex items-center justify-between text-white">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-map text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Interactive Route Map</h2>
|
||||
<p class="text-teal-100">Explore the full route</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="expandMap()"
|
||||
class="px-4 py-2 bg-white/20 backdrop-blur-sm border border-white/30 text-white font-semibold rounded-lg hover:bg-white/30 transition-all duration-200">
|
||||
<i class="fas fa-expand mr-2"></i>
|
||||
Expand
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="relative">
|
||||
<div id="interactive-map" class="map-container shadow-lg">
|
||||
<!-- Fallback content if map doesn't load -->
|
||||
<div id="map-fallback" class="absolute inset-0 flex items-center justify-center bg-gray-100 text-gray-600">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-map text-4xl mb-4 text-gray-400"></i>
|
||||
<div class="font-semibold">Loading Interactive Map...</div>
|
||||
<div class="text-sm mt-2">If map doesn't load, please refresh the page</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="map-loading" class="map-loading">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-teal-600"></div>
|
||||
<span class="text-gray-700 font-medium">Loading route...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Interactive Map Card removed as requested -->
|
||||
{% endif %}
|
||||
|
||||
<!-- Adventure Info -->
|
||||
@@ -568,6 +551,14 @@
|
||||
<span class="text-gray-600">Photos</span>
|
||||
<span class="text-gray-900">{{ post.images.count() }}</span>
|
||||
</div>
|
||||
<!-- Interactive Map Card (iframe, styled as in community index) -->
|
||||
<div class="map-card-square mt-8 mb-8 mx-auto">
|
||||
<div class="map-card-header">
|
||||
<span class="map-card-title">🗺️ Interactive Route Map</span>
|
||||
<span class="map-card-count">1 route</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -908,7 +899,6 @@ function toggleLike(postId) {
|
||||
likeBtn.classList.add('from-pink-600', 'to-red-600', 'hover:from-pink-700', 'hover:to-red-700');
|
||||
likeText.textContent = 'Like this Adventure';
|
||||
}
|
||||
|
||||
// Update like count in meta section
|
||||
const likeCountSpan = document.querySelector('.bg-pink-500\\/20');
|
||||
if (likeCountSpan) {
|
||||
|
||||
@@ -1,941 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ post.title }} - Moto Adventure{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin-bottom: -4rem;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
filter: brightness(0.7);
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.9));
|
||||
color: white;
|
||||
padding: 3rem 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 50px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from { box-shadow: 0 0 20px rgba(255,255,255,0.3); }
|
||||
to { box-shadow: 0 0 30px rgba(255,255,255,0.5); }
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 4px 8px rgba(0,0,0,0.5);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.4rem;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
line-height: 1.8;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.content-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.media-gallery {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.gallery-image:hover {
|
||||
transform: translateY(-10px) scale(1.02);
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.gallery-image img {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.gallery-image:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.gallery-image:hover .image-overlay {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.map-container {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 450px;
|
||||
border-radius: 15px;
|
||||
border: 3px solid #e9ecef;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.gpx-info {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
margin-top: 2rem;
|
||||
border-left: 6px solid #007bff;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
border: 2px solid #e9ecef;
|
||||
transition: all 0.4s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: #007bff;
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 15px 40px rgba(0,123,255,0.2);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
color: #007bff;
|
||||
display: block;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 15px;
|
||||
border-left: 6px solid #28a745;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.author-details h5 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.author-date {
|
||||
color: #6c757d;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.post-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-left: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||
color: white;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 5px 15px rgba(0,123,255,0.3);
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.stat-badge:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
|
||||
box-shadow: 0 5px 15px rgba(40,167,69,0.3);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
border-radius: 50px;
|
||||
padding: 1rem 2.5rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: all 0.4s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 8px 25px rgba(40,167,69,0.3);
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: linear-gradient(135deg, #20c997 0%, #28a745 100%);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 35px rgba(40,167,69,0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-like {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 8px 25px rgba(220,53,69,0.3);
|
||||
}
|
||||
|
||||
.btn-like:hover {
|
||||
background: linear-gradient(135deg, #fd7e14 0%, #dc3545 100%);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 35px rgba(220,53,69,0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-like.liked {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
animation: pulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 50px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: rgba(255,193,7,0.9);
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.status-published {
|
||||
background: rgba(40,167,69,0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 3rem;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-left: 6px solid #007bff;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
border-radius: 0 15px 15px 0;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.comment:hover {
|
||||
transform: translateX(10px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
float: right;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
color: #495057;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
height: 50vh;
|
||||
margin-bottom: -2rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.post-meta, .post-content, .media-gallery, .map-container, .comments-section {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-stats {
|
||||
margin-left: 0;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid p-0">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-section">
|
||||
{% set cover_image = post.images.filter_by(is_cover=True).first() %}
|
||||
{% if cover_image %}
|
||||
<img src="{{ cover_image.get_url() }}" alt="{{ post.title }}" class="hero-image">
|
||||
{% endif %}
|
||||
|
||||
<!-- Status Badge -->
|
||||
{% if current_user.is_authenticated and (current_user.id == post.author_id or current_user.is_admin) %}
|
||||
<div class="status-badge {{ 'status-published' if post.published else 'status-pending' }}">
|
||||
{% if post.published %}
|
||||
<i class="fas fa-check-circle"></i> Published
|
||||
{% else %}
|
||||
<i class="fas fa-clock"></i> Pending Review
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="hero-overlay">
|
||||
<div class="container hero-content">
|
||||
<div class="difficulty-badge">
|
||||
{% for i in range(post.difficulty) %}
|
||||
<i class="fas fa-star"></i>
|
||||
{% endfor %}
|
||||
{{ post.get_difficulty_label() }}
|
||||
</div>
|
||||
<h1 class="hero-title">{{ post.title }}</h1>
|
||||
{% if post.subtitle %}
|
||||
<p class="hero-subtitle">{{ post.subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container content-wrapper">
|
||||
<!-- Post Meta Information -->
|
||||
<div class="post-meta">
|
||||
<div class="author-info">
|
||||
<div class="author-avatar">
|
||||
{{ post.author.nickname[0].upper() }}
|
||||
</div>
|
||||
<div class="author-details">
|
||||
<h5>{{ post.author.nickname }}</h5>
|
||||
<div class="author-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
Published on {{ post.created_at.strftime('%B %d, %Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-stats">
|
||||
<span class="stat-badge">
|
||||
<i class="fas fa-heart"></i> {{ post.get_like_count() }} likes
|
||||
</span>
|
||||
<span class="stat-badge">
|
||||
<i class="fas fa-comments"></i> {{ post.comments.count() }} comments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Content -->
|
||||
<div class="post-content">
|
||||
<div class="content-header">
|
||||
<div class="content-icon">
|
||||
<i class="fas fa-book-open"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="mb-0">Adventure Story</h2>
|
||||
<p class="text-muted mb-0">Discover the journey through the author's words</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-text">
|
||||
{{ post.content | safe | nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Gallery -->
|
||||
{% if post.images.count() > 0 %}
|
||||
<div class="media-gallery">
|
||||
<div class="content-header">
|
||||
<div class="content-icon">
|
||||
<i class="fas fa-camera-retro"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="mb-0">Photo Gallery</h2>
|
||||
<p class="text-muted mb-0">Visual highlights from this adventure</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-grid">
|
||||
{% for image in post.images %}
|
||||
<div class="gallery-image" onclick="openImageModal('{{ image.get_url() }}', '{{ image.description or image.original_name }}')">
|
||||
<img src="{{ image.get_url() }}" alt="{{ image.description or image.original_name }}" loading="lazy">
|
||||
{% if image.description %}
|
||||
<div class="image-overlay">
|
||||
<p class="mb-0"><i class="fas fa-info-circle"></i> {{ image.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if image.is_cover %}
|
||||
<div class="position-absolute top-0 start-0 m-3">
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="fas fa-star"></i> Cover
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- GPX Map and Route Information -->
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
<div class="map-container">
|
||||
<div class="content-header">
|
||||
<div class="content-icon">
|
||||
<i class="fas fa-route"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="mb-0">Interactive Route Map</h2>
|
||||
<p class="text-muted mb-0">Explore the GPS track and route statistics</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="gpx-info">
|
||||
<h4 class="mb-3">
|
||||
<i class="fas fa-chart-line"></i> Route Statistics
|
||||
</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value" id="distance">-</span>
|
||||
<div class="stat-label">
|
||||
<i class="fas fa-road"></i> Distance (km)
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value" id="elevation-gain">-</span>
|
||||
<div class="stat-label">
|
||||
<i class="fas fa-mountain"></i> Elevation Gain (m)
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value" id="max-elevation">-</span>
|
||||
<div class="stat-label">
|
||||
<i class="fas fa-arrow-up"></i> Max Elevation (m)
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value" id="waypoints">-</span>
|
||||
<div class="stat-label">
|
||||
<i class="fas fa-map-pin"></i> Track Points
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
{% for gpx_file in post.gpx_files %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ gpx_file.get_url() }}" download="{{ gpx_file.original_name }}" class="btn-action btn-download">
|
||||
<i class="fas fa-download"></i>
|
||||
Download GPX ({{ "%.1f"|format(gpx_file.size / 1024) }} KB)
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="btn-action btn-download">
|
||||
<i class="fas fa-lock"></i>
|
||||
Login to Download GPX
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<button class="btn-action btn-like" onclick="toggleLike({{ post.id }})">
|
||||
<i class="fas fa-heart"></i>
|
||||
<span id="like-text">Like this Adventure</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="comments-section">
|
||||
<div class="content-header">
|
||||
<div class="content-icon">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="mb-0">Community Discussion</h2>
|
||||
<p class="text-muted mb-0">Share your thoughts and experiences ({{ comments|length }})</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="comment-form-wrapper">
|
||||
<form method="POST" action="{{ url_for('community.add_comment', id=post.id) }}" class="mb-4">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.content.label(class="form-label fw-bold") }}
|
||||
{{ form.content(class="form-control", rows="4", placeholder="Share your thoughts about this adventure, ask questions, or provide helpful tips...") }}
|
||||
</div>
|
||||
<button type="submit" class="btn-action btn-download">
|
||||
<i class="fas fa-paper-plane"></i> Post Comment
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info d-flex align-items-center">
|
||||
<i class="fas fa-info-circle me-3 fs-4"></i>
|
||||
<div>
|
||||
<strong>Join the Discussion!</strong>
|
||||
<p class="mb-0">
|
||||
<a href="{{ url_for('auth.login') }}" class="alert-link">Login</a> or
|
||||
<a href="{{ url_for('auth.register') }}" class="alert-link">create an account</a>
|
||||
to leave a comment and join the adventure community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="comments-list">
|
||||
{% for comment in comments %}
|
||||
<div class="comment">
|
||||
<div class="comment-author">
|
||||
<i class="fas fa-user-circle me-2"></i>
|
||||
{{ comment.author.nickname }}
|
||||
<span class="comment-date">
|
||||
<i class="fas fa-clock"></i>
|
||||
{{ comment.created_at.strftime('%B %d, %Y at %I:%M %p') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ comment.content }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if comments|length == 0 %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-comment-slash"></i>
|
||||
<h5>No comments yet</h5>
|
||||
<p>Be the first to share your thoughts about this adventure!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Modal -->
|
||||
<div class="modal fade" id="imageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="imageModalLabel">
|
||||
<i class="fas fa-image me-2"></i>Image Gallery
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center p-0">
|
||||
<img id="modalImage" src="" alt="" class="img-fluid rounded">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
let map;
|
||||
let gpxLayer;
|
||||
|
||||
// Initialize map if GPX files exist
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize map
|
||||
map = L.map('map').setView([45.9432, 24.9668], 10); // Romania center as default
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Load and display GPX files
|
||||
{% for gpx_file in post.gpx_files %}
|
||||
loadGPXFile('{{ gpx_file.get_url() }}');
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
function loadGPXFile(gpxUrl) {
|
||||
fetch(gpxUrl)
|
||||
.then(response => response.text())
|
||||
.then(gpxContent => {
|
||||
const parser = new DOMParser();
|
||||
const gpxDoc = parser.parseFromString(gpxContent, 'application/xml');
|
||||
|
||||
// Parse track points
|
||||
const trackPoints = [];
|
||||
const trkpts = gpxDoc.getElementsByTagName('trkpt');
|
||||
let totalDistance = 0;
|
||||
let elevationGain = 0;
|
||||
let maxElevation = 0;
|
||||
let previousPoint = null;
|
||||
|
||||
for (let i = 0; i < trkpts.length; i++) {
|
||||
const lat = parseFloat(trkpts[i].getAttribute('lat'));
|
||||
const lon = parseFloat(trkpts[i].getAttribute('lon'));
|
||||
const eleElement = trkpts[i].getElementsByTagName('ele')[0];
|
||||
const elevation = eleElement ? parseFloat(eleElement.textContent) : 0;
|
||||
|
||||
trackPoints.push([lat, lon]);
|
||||
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation;
|
||||
}
|
||||
|
||||
if (previousPoint) {
|
||||
// Calculate distance
|
||||
const distance = calculateDistance(previousPoint.lat, previousPoint.lon, lat, lon);
|
||||
totalDistance += distance;
|
||||
|
||||
// Calculate elevation gain
|
||||
if (elevation > previousPoint.elevation) {
|
||||
elevationGain += (elevation - previousPoint.elevation);
|
||||
}
|
||||
}
|
||||
|
||||
previousPoint = { lat: lat, lon: lon, elevation: elevation };
|
||||
}
|
||||
|
||||
// Add track to map
|
||||
if (trackPoints.length > 0) {
|
||||
gpxLayer = L.polyline(trackPoints, {
|
||||
color: '#e74c3c',
|
||||
weight: 4,
|
||||
opacity: 0.8
|
||||
}).addTo(map);
|
||||
|
||||
// Fit map to track
|
||||
map.fitBounds(gpxLayer.getBounds(), { padding: [20, 20] });
|
||||
|
||||
// Add start and end markers
|
||||
const startIcon = L.divIcon({
|
||||
html: '<i class="fas fa-play" style="color: green; font-size: 20px;"></i>',
|
||||
iconSize: [30, 30],
|
||||
className: 'custom-div-icon'
|
||||
});
|
||||
|
||||
const endIcon = L.divIcon({
|
||||
html: '<i class="fas fa-flag-checkered" style="color: red; font-size: 20px;"></i>',
|
||||
iconSize: [30, 30],
|
||||
className: 'custom-div-icon'
|
||||
});
|
||||
|
||||
L.marker(trackPoints[0], { icon: startIcon })
|
||||
.bindPopup('Start Point')
|
||||
.addTo(map);
|
||||
|
||||
L.marker(trackPoints[trackPoints.length - 1], { icon: endIcon })
|
||||
.bindPopup('End Point')
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
document.getElementById('distance').textContent = totalDistance.toFixed(1);
|
||||
document.getElementById('elevation-gain').textContent = Math.round(elevationGain);
|
||||
document.getElementById('max-elevation').textContent = Math.round(maxElevation);
|
||||
document.getElementById('waypoints').textContent = trackPoints.length;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading GPX file:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // Earth's radius in km
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
return R * c;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Image modal functionality
|
||||
function openImageModal(imageSrc, imageTitle) {
|
||||
document.getElementById('modalImage').src = imageSrc;
|
||||
document.getElementById('imageModalLabel').textContent = imageTitle;
|
||||
new bootstrap.Modal(document.getElementById('imageModal')).show();
|
||||
}
|
||||
|
||||
// Like functionality
|
||||
function toggleLike(postId) {
|
||||
fetch(`/community/post/${postId}/like`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const likeBtn = document.querySelector('.btn-like');
|
||||
const likeText = document.getElementById('like-text');
|
||||
|
||||
if (data.liked) {
|
||||
likeBtn.classList.add('liked');
|
||||
likeText.textContent = 'Liked!';
|
||||
} else {
|
||||
likeBtn.classList.remove('liked');
|
||||
likeText.textContent = 'Like this Adventure';
|
||||
}
|
||||
|
||||
// Update like count
|
||||
const likeCountBadge = document.querySelector('.stat-badge');
|
||||
likeCountBadge.innerHTML = `<i class="fas fa-heart"></i> ${data.count} likes`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,521 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ post.title }} - Moto Adventure{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<style>
|
||||
.map-container {
|
||||
height: 400px;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 pt-16">
|
||||
<!-- Hero Section -->
|
||||
<div class="relative overflow-hidden py-16">
|
||||
{% set cover_image = post.images.filter_by(is_cover=True).first() %}
|
||||
{% if cover_image %}
|
||||
<div class="absolute inset-0">
|
||||
<img src="{{ cover_image.get_url() }}" alt="{{ post.title }}"
|
||||
class="w-full h-full object-cover opacity-30">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status Badge -->
|
||||
{% if current_user.is_authenticated and (current_user.id == post.author_id or current_user.is_admin) %}
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
|
||||
{{ 'bg-green-100 text-green-800' if post.published else 'bg-yellow-100 text-yellow-800' }}">
|
||||
{% if post.published %}
|
||||
<i class="fas fa-check-circle mr-1"></i> Published
|
||||
{% else %}
|
||||
<i class="fas fa-clock mr-1"></i> Pending Review
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<!-- Difficulty Badge -->
|
||||
<div class="inline-flex items-center px-4 py-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 text-white mb-6">
|
||||
{% for i in range(post.difficulty) %}
|
||||
<i class="fas fa-star text-yellow-400 mr-1"></i>
|
||||
{% endfor %}
|
||||
<span class="ml-2 font-semibold">{{ post.get_difficulty_label() }}</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-white mb-4">{{ post.title }}</h1>
|
||||
{% if post.subtitle %}
|
||||
<p class="text-xl text-blue-100 max-w-3xl mx-auto">{{ post.subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12">
|
||||
<!-- Post Meta Information -->
|
||||
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20 mb-8 -mt-8 relative z-10">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-gradient-to-r from-orange-500 to-red-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
|
||||
{{ post.author.nickname[0].upper() }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold text-lg">{{ post.author.nickname }}</h3>
|
||||
<p class="text-blue-200 text-sm">
|
||||
<i class="fas fa-calendar-alt mr-1"></i>
|
||||
Published on {{ post.created_at.strftime('%B %d, %Y') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full bg-pink-500/20 text-pink-200 text-sm">
|
||||
<i class="fas fa-heart mr-1"></i> {{ post.get_like_count() }} likes
|
||||
</span>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full bg-blue-500/20 text-blue-200 text-sm">
|
||||
<i class="fas fa-comments mr-1"></i> {{ post.comments.count() }} comments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<!-- Adventure Story -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-book-open text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Adventure Story</h2>
|
||||
<p class="text-blue-100">Discover the journey through the author's words</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="prose prose-lg max-w-none text-gray-700">
|
||||
{{ post.content | safe | nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo Gallery -->
|
||||
{% if post.images.count() > 0 %}
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-green-600 to-teal-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-camera-retro text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Photo Gallery</h2>
|
||||
<p class="text-green-100">Visual highlights from this adventure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{% for image in post.images %}
|
||||
<div class="relative rounded-lg overflow-hidden cursor-pointer group transition-transform duration-300 hover:scale-105"
|
||||
onclick="openImageModal('{{ image.get_url() }}', '{{ image.description or image.original_name }}')">
|
||||
<img src="{{ image.get_url() }}" alt="{{ image.description or image.original_name }}"
|
||||
class="w-full h-64 object-cover">
|
||||
{% if image.description %}
|
||||
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end">
|
||||
<p class="text-white p-4 text-sm">{{ image.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if image.is_cover %}
|
||||
<div class="absolute top-2 left-2">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded bg-yellow-500 text-white text-xs font-semibold">
|
||||
<i class="fas fa-star mr-1"></i> Cover
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-comment-dots text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Community Discussion</h2>
|
||||
<p class="text-purple-100">Share your thoughts and experiences ({{ comments|length }})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
{% if current_user.is_authenticated %}
|
||||
<form method="POST" action="{{ url_for('community.add_comment', id=post.id) }}" class="mb-6">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-4">
|
||||
{{ form.content.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.content(class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent", rows="4", placeholder="Share your thoughts about this adventure...") }}
|
||||
</div>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200">
|
||||
<i class="fas fa-paper-plane mr-2"></i> Post Comment
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-info-circle text-blue-600 mr-3"></i>
|
||||
<div>
|
||||
<p class="text-blue-800">
|
||||
<a href="{{ url_for('auth.login') }}" class="font-semibold hover:underline">Login</a> or
|
||||
<a href="{{ url_for('auth.register') }}" class="font-semibold hover:underline">create an account</a>
|
||||
to join the discussion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="space-y-4">
|
||||
{% for comment in comments %}
|
||||
<div class="bg-gray-50 rounded-lg p-4 border-l-4 border-blue-500">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-semibold text-gray-900">{{ comment.author.nickname }}</h4>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ comment.created_at.strftime('%B %d, %Y at %I:%M %p') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-700">{{ comment.content }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if comments|length == 0 %}
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<i class="fas fa-comment-slash text-4xl mb-4 opacity-50"></i>
|
||||
<h5 class="text-lg font-semibold mb-2">No comments yet</h5>
|
||||
<p>Be the first to share your thoughts about this adventure!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-8">
|
||||
<!-- GPS Map and Route Information -->
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-orange-600 to-red-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-route text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Route Map</h2>
|
||||
<p class="text-orange-100">GPS track and statistics</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="map" class="map-container mb-6 shadow-lg"></div>
|
||||
|
||||
<!-- Route Statistics -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
|
||||
<div class="text-2xl font-bold text-blue-600" id="distance">-</div>
|
||||
<div class="text-sm text-gray-600">Distance (km)</div>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
|
||||
<div class="text-2xl font-bold text-green-600" id="elevation-gain">-</div>
|
||||
<div class="text-sm text-gray-600">Elevation (m)</div>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
|
||||
<div class="text-2xl font-bold text-purple-600" id="max-elevation">-</div>
|
||||
<div class="text-sm text-gray-600">Max Elevation (m)</div>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg">
|
||||
<div class="text-2xl font-bold text-orange-600" id="waypoints">-</div>
|
||||
<div class="text-sm text-gray-600">Track Points</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-3">
|
||||
{% for gpx_file in post.gpx_files %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ gpx_file.get_url() }}" download="{{ gpx_file.original_name }}"
|
||||
class="block w-full text-center px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white font-semibold rounded-lg hover:from-green-700 hover:to-emerald-700 transition-all duration-200 transform hover:scale-105">
|
||||
<i class="fas fa-download mr-2"></i>
|
||||
Download GPX ({{ "%.1f"|format(gpx_file.size / 1024) }} KB)
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}"
|
||||
class="block w-full text-center px-4 py-2 bg-gray-400 text-white font-semibold rounded-lg hover:bg-gray-500 transition-all duration-200">
|
||||
<i class="fas fa-lock mr-2"></i>
|
||||
Login to Download GPX
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<button onclick="toggleLike({{ post.id }})"
|
||||
class="w-full px-4 py-2 bg-gradient-to-r from-pink-600 to-red-600 text-white font-semibold rounded-lg hover:from-pink-700 hover:to-red-700 transition-all duration-200 transform hover:scale-105">
|
||||
<i class="fas fa-heart mr-2"></i>
|
||||
<span id="like-text">Like this Adventure</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Adventure Info -->
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="bg-gradient-to-r from-indigo-600 to-blue-600 p-6">
|
||||
<div class="flex items-center text-white">
|
||||
<i class="fas fa-info-circle text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Adventure Info</h2>
|
||||
<p class="text-indigo-100">Trip details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Difficulty</span>
|
||||
<div class="flex items-center">
|
||||
{% for i in range(post.difficulty) %}
|
||||
<i class="fas fa-star text-yellow-500"></i>
|
||||
{% endfor %}
|
||||
{% for i in range(5 - post.difficulty) %}
|
||||
<i class="far fa-star text-gray-300"></i>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Published</span>
|
||||
<span class="text-gray-900">{{ post.created_at.strftime('%B %d, %Y') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Author</span>
|
||||
<span class="text-gray-900">{{ post.author.nickname }}</span>
|
||||
</div>
|
||||
{% if post.images.count() > 0 %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Photos</span>
|
||||
<span class="text-gray-900">{{ post.images.count() }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">GPS Files</span>
|
||||
<span class="text-gray-900">{{ post.gpx_files.count() }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Modal -->
|
||||
<div id="imageModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
|
||||
<div class="relative max-w-4xl max-h-full p-4">
|
||||
<button onclick="closeImageModal()" class="absolute top-2 right-2 text-white text-2xl z-10 bg-black bg-opacity-50 w-10 h-10 rounded-full flex items-center justify-center hover:bg-opacity-75">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<img id="modalImage" src="" alt="" class="max-w-full max-h-full rounded-lg">
|
||||
<div id="modalCaption" class="text-white text-center mt-4 text-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
let map;
|
||||
|
||||
// Initialize map if GPX files exist
|
||||
{% if post.gpx_files.count() > 0 %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize map
|
||||
map = L.map('map').setView([45.9432, 24.9668], 10); // Romania center as default
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Load and display GPX files
|
||||
{% for gpx_file in post.gpx_files %}
|
||||
loadGPXFile('{{ gpx_file.get_url() }}');
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
function loadGPXFile(gpxUrl) {
|
||||
fetch(gpxUrl)
|
||||
.then(response => response.text())
|
||||
.then(gpxContent => {
|
||||
const parser = new DOMParser();
|
||||
const gpxDoc = parser.parseFromString(gpxContent, 'application/xml');
|
||||
|
||||
// Parse track points
|
||||
const trackPoints = [];
|
||||
const trkpts = gpxDoc.getElementsByTagName('trkpt');
|
||||
let totalDistance = 0;
|
||||
let elevationGain = 0;
|
||||
let maxElevation = 0;
|
||||
let previousPoint = null;
|
||||
|
||||
for (let i = 0; i < trkpts.length; i++) {
|
||||
const lat = parseFloat(trkpts[i].getAttribute('lat'));
|
||||
const lon = parseFloat(trkpts[i].getAttribute('lon'));
|
||||
const eleElement = trkpts[i].getElementsByTagName('ele')[0];
|
||||
const elevation = eleElement ? parseFloat(eleElement.textContent) : 0;
|
||||
|
||||
trackPoints.push([lat, lon]);
|
||||
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation;
|
||||
}
|
||||
|
||||
if (previousPoint) {
|
||||
// Calculate distance
|
||||
const distance = calculateDistance(previousPoint.lat, previousPoint.lon, lat, lon);
|
||||
totalDistance += distance;
|
||||
|
||||
// Calculate elevation gain
|
||||
if (elevation > previousPoint.elevation) {
|
||||
elevationGain += (elevation - previousPoint.elevation);
|
||||
}
|
||||
}
|
||||
|
||||
previousPoint = { lat: lat, lon: lon, elevation: elevation };
|
||||
}
|
||||
|
||||
// Add track to map
|
||||
if (trackPoints.length > 0) {
|
||||
const polyline = L.polyline(trackPoints, {
|
||||
color: '#e74c3c',
|
||||
weight: 4,
|
||||
opacity: 0.8
|
||||
}).addTo(map);
|
||||
|
||||
// Fit map to track
|
||||
map.fitBounds(polyline.getBounds(), { padding: [20, 20] });
|
||||
|
||||
// Add start and end markers
|
||||
const startIcon = L.divIcon({
|
||||
html: '<div class="w-6 h-6 bg-green-500 rounded-full border-2 border-white flex items-center justify-center text-white text-xs font-bold">S</div>',
|
||||
className: 'custom-div-icon',
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
|
||||
const endIcon = L.divIcon({
|
||||
html: '<div class="w-6 h-6 bg-red-500 rounded-full border-2 border-white flex items-center justify-center text-white text-xs font-bold">E</div>',
|
||||
className: 'custom-div-icon',
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
|
||||
L.marker(trackPoints[0], { icon: startIcon })
|
||||
.bindPopup('Start Point')
|
||||
.addTo(map);
|
||||
|
||||
L.marker(trackPoints[trackPoints.length - 1], { icon: endIcon })
|
||||
.bindPopup('End Point')
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
document.getElementById('distance').textContent = totalDistance.toFixed(1);
|
||||
document.getElementById('elevation-gain').textContent = Math.round(elevationGain);
|
||||
document.getElementById('max-elevation').textContent = Math.round(maxElevation);
|
||||
document.getElementById('waypoints').textContent = trackPoints.length;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading GPX file:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // Earth's radius in km
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
return R * c;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Image modal functionality
|
||||
function openImageModal(imageSrc, imageTitle) {
|
||||
document.getElementById('modalImage').src = imageSrc;
|
||||
document.getElementById('modalCaption').textContent = imageTitle;
|
||||
document.getElementById('imageModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
document.getElementById('imageModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Close modal on click outside
|
||||
document.getElementById('imageModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeImageModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeImageModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Like functionality
|
||||
function toggleLike(postId) {
|
||||
fetch(`/community/post/${postId}/like`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const likeBtn = document.querySelector('button[onclick*="toggleLike"]');
|
||||
const likeText = document.getElementById('like-text');
|
||||
|
||||
if (data.liked) {
|
||||
likeBtn.classList.remove('from-pink-600', 'to-red-600', 'hover:from-pink-700', 'hover:to-red-700');
|
||||
likeBtn.classList.add('from-red-600', 'to-pink-600', 'hover:from-red-700', 'hover:to-pink-700');
|
||||
likeText.textContent = 'Liked!';
|
||||
} else {
|
||||
likeBtn.classList.remove('from-red-600', 'to-pink-600', 'hover:from-red-700', 'hover:to-pink-700');
|
||||
likeBtn.classList.add('from-pink-600', 'to-red-600', 'hover:from-pink-700', 'hover:to-red-700');
|
||||
likeText.textContent = 'Like this Adventure';
|
||||
}
|
||||
|
||||
// Update like count in meta section
|
||||
const likeCountSpan = document.querySelector('.bg-pink-500\\/20');
|
||||
if (likeCountSpan) {
|
||||
likeCountSpan.innerHTML = `<i class="fas fa-heart mr-1"></i> ${data.count} likes`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Please log in to like posts');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user