Files
moto-adv-website/app/templates/community/post_detail_fixed.html
ske087 60ef02ced9 Major UI/UX redesign and feature enhancements
🎨 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
2025-07-24 02:44:25 +03:00

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 %}