🎨 Complete Tailwind CSS conversion - Redesigned post detail page with modern gradient backgrounds - Updated profile page with consistent design language - Converted from Bootstrap to Tailwind CSS throughout ✨ New Features & Improvements - Enhanced community post management system - Added admin panel with analytics dashboard - Improved post creation and editing workflows - Interactive GPS map integration with Leaflet.js - Photo gallery with modal view and hover effects - Adventure statistics and metadata display - Like system and community engagement features 🔧 Technical Improvements - Fixed template syntax errors and CSRF token issues - Updated database models and relationships - Enhanced media file management - Improved responsive design patterns - Added proper error handling and validation 📱 Mobile-First Design - Responsive grid layouts - Touch-friendly interactions - Optimized for all screen sizes - Modern card-based UI components 🏍️ Adventure Platform Features - GPS track visualization and statistics - Photo uploads with thumbnail generation - GPX file downloads for registered users - Community comments and discussions - Post approval workflow for admins - Difficulty rating system with star indicators
942 lines
30 KiB
HTML
942 lines
30 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" />
|
|
<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 %}
|