Critical Fixes: 🗺️ GPX Statistics Processing: - Fixed Docker volume mapping from ./static/media to ./app/static/media - GPX files now properly accessible in container for statistics calculation - GPS route statistics (distance, elevation, track points) now display correctly - Added fix_gpx_statistics.py utility script for reprocessing existing GPX files 🐛 Template Fixes: - Fixed CSRF token undefined error in post_detail.html template - Resolved 500 errors when accessing community post pages - Template now uses form.csrf_token instead of csrf_token() function �� Docker Improvements: - Corrected volume mounting to ensure GPX file persistence - Fixed path resolution for media files in containerized environment - New posts will now properly save and process GPX files ✅ Verified Functionality: - Community post pages load successfully (200 OK) - GPS statistics display correctly (50.1 km distance, 2231 track points) - Future posts will automatically calculate and display GPX statistics - Docker container properly syncs with host filesystem This update ensures the motorcycle adventure platform's GPS tracking and route statistics work reliably in production.
918 lines
44 KiB
HTML
918 lines
44 KiB
HTML
{% 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"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""/>
|
|
<style>
|
|
.map-container {
|
|
height: 400px !important;
|
|
min-height: 400px;
|
|
width: 100%;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
border: 2px solid #e5e7eb;
|
|
background-color: #f8f9fa;
|
|
position: relative;
|
|
}
|
|
|
|
/* Ensure Leaflet map fills container */
|
|
.leaflet-container {
|
|
height: 100% !important;
|
|
width: 100% !important;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
/* Interactive map specific styling */
|
|
#interactive-map {
|
|
height: 400px !important;
|
|
min-height: 400px;
|
|
width: 100%;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
border: 2px solid #e5e7eb;
|
|
position: relative;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
/* Force map to be visible */
|
|
#interactive-map .leaflet-container {
|
|
height: 400px !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
/* Custom div icon styling */
|
|
.custom-div-icon {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
}
|
|
|
|
/* Expanded map modal styling */
|
|
#expanded-map {
|
|
height: 100%;
|
|
min-height: 70vh;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
/* Interactive map container */
|
|
#interactive-map {
|
|
height: 400px;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
border: 2px solid #e5e7eb;
|
|
position: relative;
|
|
}
|
|
|
|
/* Modal animations */
|
|
#expandedMapModal {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
#expandedMapModal.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Loading indicator styling */
|
|
.map-loading {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 1rem 1.5rem;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
z-index: 1000;
|
|
}
|
|
|
|
/* Custom leaflet control styling */
|
|
.leaflet-control-zoom {
|
|
border: none !important;
|
|
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16) !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a {
|
|
border-radius: 4px !important;
|
|
border: 1px solid #ddd !important;
|
|
background-color: white !important;
|
|
color: #333 !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a:hover {
|
|
background-color: #f5f5f5 !important;
|
|
}
|
|
|
|
/* Ensure map tiles load properly */
|
|
.leaflet-container {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
/* Leaflet popup custom styling */
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
margin: 8px 12px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Custom zoom control styling */
|
|
.leaflet-control-zoom {
|
|
border: none !important;
|
|
border-radius: 8px !important;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a {
|
|
border-radius: 4px !important;
|
|
color: #374151 !important;
|
|
font-weight: bold !important;
|
|
}
|
|
|
|
.leaflet-control-zoom a:hover {
|
|
background-color: #f3f4f6 !important;
|
|
color: #1f2937 !important;
|
|
}
|
|
|
|
/* Scale control styling */
|
|
.leaflet-control-scale {
|
|
background-color: rgba(255, 255, 255, 0.9) !important;
|
|
border-radius: 4px !important;
|
|
padding: 2px 6px !important;
|
|
font-size: 11px !important;
|
|
}
|
|
|
|
/* Map loading indicator */
|
|
.map-loading {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 1000;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
</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">
|
|
<!-- Adventure Story Blog Post -->
|
|
<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">Follow the journey step by step</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-8">
|
|
<div class="prose prose-lg max-w-none">
|
|
{% set content_sections = post.content.split('\n\n') %}
|
|
{% set section_images = post.images.filter_by(is_cover=False).all() %}
|
|
{% set images_per_section = (section_images|length / (content_sections|length))|round|int if content_sections|length > 0 else 0 %}
|
|
|
|
{% for section in content_sections %}
|
|
{% if section.strip() %}
|
|
<div class="mb-8 pb-6 {% if not loop.last %}border-b border-gray-200{% endif %}">
|
|
{% set section_text = section.strip() %}
|
|
|
|
<!-- Extract keywords and clean text -->
|
|
{% set keywords = [] %}
|
|
{% set clean_text = section_text %}
|
|
|
|
<!-- Process **keyword** patterns and standalone keywords -->
|
|
{% if '**' in section_text %}
|
|
{% set parts = section_text.split('**') %}
|
|
{% set clean_parts = [] %}
|
|
{% for i in range(parts|length) %}
|
|
{% if i % 2 == 1 %}
|
|
<!-- This is a keyword between ** ** -->
|
|
{% set _ = keywords.append(parts[i].strip()) %}
|
|
{% else %}
|
|
<!-- This is regular text -->
|
|
{% set _ = clean_parts.append(parts[i]) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% set clean_text = clean_parts|join(' ')|trim %}
|
|
{% endif %}
|
|
|
|
<!-- Also extract standalone keywords (before <br><br>) -->
|
|
{% if '<br>' in clean_text %}
|
|
{% set text_parts = clean_text.split('<br>') %}
|
|
{% set first_part = text_parts[0].strip() %}
|
|
|
|
<!-- If first part looks like keywords (short words), extract them -->
|
|
{% if first_part and first_part|length < 100 and ' ' in first_part %}
|
|
{% set potential_keywords = first_part.split() %}
|
|
{% if potential_keywords|length <= 5 %}
|
|
<!-- Likely keywords, add them and remove from text -->
|
|
{% for kw in potential_keywords %}
|
|
{% if kw.strip() %}
|
|
{% set _ = keywords.append(kw.strip()) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
<!-- Keep only text after the first <br><br> -->
|
|
{% set remaining_parts = text_parts[1:] %}
|
|
{% set clean_text = remaining_parts|join('<br>')|trim %}
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<!-- Clean up multiple <br> tags -->
|
|
{% set clean_text = clean_text.replace('<br><br><br>', '<br><br>').replace('<br> <br>', '<br><br>').strip() %}
|
|
|
|
<!-- Section Keywords -->
|
|
{% if keywords %}
|
|
<div class="mb-4">
|
|
{% for keyword in keywords %}
|
|
{% if keyword.strip() %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gradient-to-r from-amber-500 to-orange-500 text-white mr-2 mb-2">
|
|
<i class="fas fa-tag mr-1 text-xs"></i>
|
|
{{ keyword.strip() }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Section Text -->
|
|
<div class="text-gray-700 leading-relaxed mb-6 text-lg">
|
|
{{ clean_text | safe | nl2br }}
|
|
</div>
|
|
|
|
<!-- Section Images -->
|
|
{% if section_images %}
|
|
{% set start_idx = loop.index0 * images_per_section %}
|
|
{% set end_idx = start_idx + images_per_section %}
|
|
{% set current_section_images = section_images[start_idx:end_idx] %}
|
|
|
|
{% if current_section_images %}
|
|
<div class="grid grid-cols-1 {% if current_section_images|length > 1 %}md:grid-cols-2{% endif %} gap-4 mb-6">
|
|
{% for image in current_section_images %}
|
|
<div class="relative rounded-xl overflow-hidden cursor-pointer group transition-all duration-300 hover:shadow-lg"
|
|
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 transition-transform duration-300 group-hover:scale-105">
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
{% if image.description %}
|
|
<div class="absolute bottom-0 left-0 right-0 p-3 text-white text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
{{ image.description }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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 %}
|
|
<!-- 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">
|
|
<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">
|
|
{% set gpx_file = post.gpx_files.first() %}
|
|
<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">
|
|
{{ gpx_file.total_distance if gpx_file and gpx_file.total_distance > 0 else '-' }}
|
|
</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">
|
|
{{ gpx_file.elevation_gain|int if gpx_file and gpx_file.elevation_gain > 0 else '-' }}
|
|
</div>
|
|
<div class="text-sm text-gray-600">Elevation Gain (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">
|
|
{{ gpx_file.max_elevation|int if gpx_file and gpx_file.max_elevation > 0 else '-' }}
|
|
</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">
|
|
{{ gpx_file.total_points if gpx_file and gpx_file.total_points > 0 else '-' }}
|
|
</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>
|
|
|
|
<!-- Interactive Map Card -->
|
|
<!-- Interactive Map Card removed as requested -->
|
|
{% 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>
|
|
<!-- 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">
|
|
<span class="text-gray-600">GPS Files</span>
|
|
<span class="text-gray-900">{{ post.gpx_files.count() }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Gallery (Compact) -->
|
|
{% 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-4">
|
|
<div class="flex items-center text-white">
|
|
<i class="fas fa-camera-retro text-xl mr-2"></i>
|
|
<div>
|
|
<h2 class="text-lg font-bold">Photo Gallery</h2>
|
|
<p class="text-green-100 text-sm">{{ post.images.count() }} photos</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="grid grid-cols-2 gap-2">
|
|
{% for image in post.images %}
|
|
<div class="relative rounded-lg overflow-hidden cursor-pointer group"
|
|
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-20 object-cover transition-transform duration-300 group-hover:scale-110">
|
|
{% if image.is_cover %}
|
|
<div class="absolute top-1 left-1">
|
|
<span class="inline-flex items-center px-1 py-0.5 rounded text-xs font-semibold bg-yellow-500 text-white">
|
|
<i class="fas fa-star"></i>
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if post.images.count() > 4 %}
|
|
<div class="mt-3 text-center">
|
|
<span class="text-sm text-gray-500">Click any photo to view gallery</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</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="max-w-4xl max-h-full p-4">
|
|
<div class="relative">
|
|
<button onclick="closeImageModal()"
|
|
class="absolute top-2 right-2 text-white bg-black bg-opacity-50 rounded-full w-8 h-8 flex items-center justify-center hover:bg-opacity-75 z-10">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
<img id="modalImage" src="" alt="" class="max-w-full max-h-screen object-contain rounded-lg">
|
|
<div id="modalCaption" class="text-white text-center mt-2 px-4"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expanded Map Modal -->
|
|
<div id="expandedMapModal" class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 hidden">
|
|
<div class="w-full h-full p-4 md:p-8 flex flex-col">
|
|
<div class="flex justify-end mb-4">
|
|
<button onclick="closeExpandedMap()"
|
|
class="text-white bg-black bg-opacity-50 rounded-full w-12 h-12 flex items-center justify-center hover:bg-opacity-75 transition-all duration-200">
|
|
<i class="fas fa-times text-xl"></i>
|
|
</button>
|
|
</div>
|
|
<div class="flex-1 bg-white rounded-lg overflow-hidden shadow-2xl">
|
|
<div id="expanded-map" class="w-full h-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat Discussion Widget -->
|
|
{% include 'chat/embed.html' %}
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
<script>
|
|
// Global variables
|
|
let map;
|
|
let interactiveMap;
|
|
let expandedMap;
|
|
let gpxPolyline;
|
|
let trackPointsData = [];
|
|
|
|
// Simple test to check if Leaflet is loaded
|
|
console.log('Leaflet loaded:', typeof L !== 'undefined');
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('DOM Content Loaded - Starting simple map test...');
|
|
|
|
// Simple map initialization
|
|
setTimeout(function() {
|
|
console.log('Attempting to create basic map...');
|
|
|
|
try {
|
|
// Create a very basic map
|
|
const mapContainer = document.getElementById('interactive-map');
|
|
if (!mapContainer) {
|
|
console.error('Map container not found');
|
|
return;
|
|
}
|
|
|
|
console.log('Container found, creating map...');
|
|
|
|
// Clear any existing content
|
|
mapContainer.innerHTML = '';
|
|
|
|
// Create map with basic settings
|
|
interactiveMap = L.map('interactive-map').setView([45.9432, 24.9668], 6);
|
|
|
|
console.log('Map created, adding tiles...');
|
|
|
|
// Add OpenStreetMap tiles
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(interactiveMap);
|
|
|
|
console.log('Tiles added, adding test marker...');
|
|
|
|
// Add a test marker
|
|
L.marker([45.9432, 24.9668])
|
|
.addTo(interactiveMap)
|
|
.bindPopup('🗺️ Interactive Map Test - Romania')
|
|
.openPopup();
|
|
|
|
console.log('✅ Basic map setup complete!');
|
|
|
|
// Hide loading indicator
|
|
const loadingDiv = document.getElementById('map-loading');
|
|
if (loadingDiv) {
|
|
loadingDiv.style.display = 'none';
|
|
}
|
|
|
|
// Hide fallback content
|
|
const fallbackDiv = document.getElementById('map-fallback');
|
|
if (fallbackDiv) {
|
|
fallbackDiv.style.display = 'none';
|
|
}
|
|
|
|
// Force resize
|
|
setTimeout(() => {
|
|
interactiveMap.invalidateSize();
|
|
console.log('Map size invalidated');
|
|
}, 500);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error creating map:', error);
|
|
|
|
// Show error in the container
|
|
const mapContainer = document.getElementById('interactive-map');
|
|
if (mapContainer) {
|
|
mapContainer.innerHTML = `
|
|
<div class="flex items-center justify-center h-full bg-red-50 text-red-600 p-4">
|
|
<div class="text-center">
|
|
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
|
|
<div class="font-semibold">Map Loading Error</div>
|
|
<div class="text-sm">${error.message}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}, 1000);
|
|
});
|
|
|
|
// Global functions that need to be available regardless of GPX files
|
|
function expandMap() {
|
|
console.log('Expand map button clicked');
|
|
const modal = document.getElementById('expandedMapModal');
|
|
if (!modal) {
|
|
console.error('Expanded map modal not found');
|
|
return;
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
// Initialize expanded map after modal is shown
|
|
setTimeout(() => {
|
|
if (!expandedMap) {
|
|
console.log('Initializing expanded map...');
|
|
expandedMap = L.map('expanded-map', {
|
|
zoomControl: true,
|
|
scrollWheelZoom: true,
|
|
doubleClickZoom: true,
|
|
boxZoom: true,
|
|
dragging: true
|
|
}).setView([45.9432, 24.9668], 6);
|
|
|
|
// Add tile layer with scale control
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 18,
|
|
minZoom: 2
|
|
}).addTo(expandedMap);
|
|
|
|
// Add scale control
|
|
L.control.scale({
|
|
position: 'bottomleft',
|
|
metric: true,
|
|
imperial: false
|
|
}).addTo(expandedMap);
|
|
}
|
|
|
|
// Clear existing layers except tile layer
|
|
expandedMap.eachLayer(function(layer) {
|
|
if (layer instanceof L.Polyline || layer instanceof L.Marker) {
|
|
expandedMap.removeLayer(layer);
|
|
}
|
|
});
|
|
|
|
// Add the GPX track to expanded map if we have track data
|
|
if (trackPointsData && trackPointsData.length > 0) {
|
|
console.log('Adding track data to expanded map...');
|
|
|
|
const expandedPolyline = L.polyline(trackPointsData, {
|
|
color: '#ef4444',
|
|
weight: 5,
|
|
opacity: 0.9,
|
|
smoothFactor: 1
|
|
}).addTo(expandedMap);
|
|
|
|
// Fit map to track
|
|
expandedMap.fitBounds(expandedPolyline.getBounds(), {
|
|
padding: [30, 30],
|
|
maxZoom: 15
|
|
});
|
|
|
|
// Add enhanced markers
|
|
const startIcon = L.divIcon({
|
|
html: '<div class="w-8 h-8 bg-green-500 rounded-full border-2 border-white flex items-center justify-center text-white shadow-lg"><i class="fas fa-play"></i></div>',
|
|
className: 'custom-div-icon',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16]
|
|
});
|
|
|
|
const endIcon = L.divIcon({
|
|
html: '<div class="w-8 h-8 bg-red-500 rounded-full border-2 border-white flex items-center justify-center text-white shadow-lg"><i class="fas fa-flag-checkered"></i></div>',
|
|
className: 'custom-div-icon',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16]
|
|
});
|
|
|
|
L.marker(trackPointsData[0], { icon: startIcon })
|
|
.bindPopup('<strong>🚀 Start Point</strong>')
|
|
.addTo(expandedMap);
|
|
|
|
L.marker(trackPointsData[trackPointsData.length - 1], { icon: endIcon })
|
|
.bindPopup('<strong>🏁 End Point</strong>')
|
|
.addTo(expandedMap);
|
|
} else {
|
|
console.log('No track data available for expanded map');
|
|
// Show a message on the map
|
|
L.popup()
|
|
.setLatLng([45.9432, 24.9668])
|
|
.setContent('<div class="text-gray-600">📍 No GPX route data available</div>')
|
|
.openOn(expandedMap);
|
|
}
|
|
|
|
// Invalidate size to ensure proper rendering
|
|
expandedMap.invalidateSize();
|
|
|
|
console.log('✓ Expanded map setup complete');
|
|
}, 100);
|
|
}
|
|
|
|
function closeExpandedMap() {
|
|
const modal = document.getElementById('expandedMapModal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// Close expanded map on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeExpandedMap();
|
|
}
|
|
});
|
|
|
|
// 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': '{{ form.csrf_token.data }}'
|
|
}
|
|
})
|
|
.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 %}
|