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
This commit is contained in:
ske087
2025-07-24 02:44:25 +03:00
parent 540eb17e89
commit 60ef02ced9
36 changed files with 12953 additions and 67 deletions

View File

@@ -0,0 +1,204 @@
{% extends "admin/base.html" %}
{% block title %}Analytics - Admin Dashboard{% endblock %}
{% block admin_content %}
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Analytics</h1>
</div>
<!-- Summary Cards Row -->
<div class="row">
<!-- Total Page Views Card -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Page Views</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ total_views }}</div>
</div>
<div class="col-auto">
<i class="fas fa-eye fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Unique Visitors Card -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Unique Visitors</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ unique_visitors }}</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Today's Views Card -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Today's Views</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ today_views }}</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-day fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- This Week Views Card -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
This Week</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ week_views }}</div>
</div>
<div class="col-auto">
<i class="fas fa-chart-line fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Popular Pages -->
<div class="row">
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Popular Pages</h6>
</div>
<div class="card-body">
{% if popular_pages %}
<div class="table-responsive">
<table class="table table-borderless">
<thead>
<tr>
<th>Page</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{% for page in popular_pages %}
<tr>
<td>{{ page.path }}</td>
<td><span class="badge bg-primary">{{ page.view_count }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No page view data available yet.</p>
{% endif %}
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="col-lg-6">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Recent Activity</h6>
</div>
<div class="card-body">
{% if recent_views %}
<div class="table-responsive">
<table class="table table-borderless">
<thead>
<tr>
<th>Time</th>
<th>Page</th>
<th>User</th>
</tr>
</thead>
<tbody>
{% for view in recent_views %}
<tr>
<td class="text-xs">{{ view.created_at.strftime('%H:%M') }}</td>
<td class="text-xs">{{ view.path }}</td>
<td class="text-xs">
{% if view.user %}
{{ view.user.nickname }}
{% else %}
Anonymous
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No recent activity data available yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Browser Stats -->
<div class="row">
<div class="col-lg-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Browser Statistics</h6>
</div>
<div class="card-body">
{% if browser_stats %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Browser</th>
<th>Views</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{% for stat in browser_stats %}
<tr>
<td>{{ stat.browser or 'Unknown' }}</td>
<td>{{ stat.view_count }}</td>
<td>
<div class="progress">
<div class="progress-bar" role="progressbar"
style="width: {{ (stat.view_count / total_views * 100) if total_views > 0 else 0 }}%">
{{ "%.1f"|format((stat.view_count / total_views * 100) if total_views > 0 else 0) }}%
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No browser data available yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin Dashboard - Moto Adventure{% endblock %}</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
font-size: 0.875rem;
background-color: #f8f9fa;
}
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
background-color: #343a40;
width: 240px;
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, .75);
padding: 10px 20px;
border-radius: 0;
}
.sidebar .nav-link:hover {
color: #fff;
background-color: rgba(255, 255, 255, .1);
}
.sidebar .nav-link.active {
color: #fff;
background-color: #007bff;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
color: rgba(255, 255, 255, .5);
padding: 15px 20px 5px;
}
.main-content {
margin-left: 240px;
padding: 20px;
min-height: 100vh;
}
.navbar-brand {
color: #fff !important;
background-color: #007bff;
padding: 10px 20px;
margin: 0;
font-weight: bold;
}
.card {
border: none;
box-shadow: 0 0.15rem 1.75rem 0 rgba(33, 40, 50, 0.15);
margin-bottom: 1.5rem;
}
.card-header {
background-color: #fff;
border-bottom: 1px solid rgba(33, 40, 50, 0.125);
font-weight: 600;
}
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.text-xs {
font-size: 0.7rem;
}
.text-gray-800 {
color: #5a5c69 !important;
}
.text-gray-300 {
color: #dddfeb !important;
}
.table th {
border-top: none;
font-weight: 600;
font-size: 0.8rem;
color: #5a5c69;
background-color: #f8f9fc;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
}
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" id="sidebar">
<div class="navbar-brand">
<i class="fas fa-cogs"></i> Admin Panel
</div>
<div class="sidebar-sticky">
<div class="sidebar-heading">
Administration
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'admin.dashboard' }}" href="{{ url_for('admin.dashboard') }}">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'admin.posts' }}" href="{{ url_for('admin.posts') }}">
<i class="fas fa-file-alt"></i> Posts
{% if pending_posts_count %}
<span class="badge bg-warning text-dark ms-2">{{ pending_posts_count }}</span>
{% endif %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'admin.users' }}" href="{{ url_for('admin.users') }}">
<i class="fas fa-users"></i> Users
</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'admin.analytics' }}" href="{{ url_for('admin.analytics') }}">
<i class="fas fa-chart-bar"></i> Analytics
</a>
</li>
</ul>
<div class="sidebar-heading">
Quick Actions
</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('community.index') }}" target="_blank">
<i class="fas fa-external-link-alt"></i> View Site
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.posts', status='pending') }}">
<i class="fas fa-clock"></i> Pending Posts
</a>
</li>
</ul>
</div>
</nav>
<!-- Main Content -->
<div class="main-content">
<!-- Mobile menu button -->
<button class="btn btn-primary d-md-none mb-3" type="button" id="sidebarToggle">
<i class="fas fa-bars"></i>
</button>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block admin_content %}{% endblock %}
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Mobile sidebar toggle
document.getElementById('sidebarToggle')?.addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('show');
});
// Auto-refresh stats every 30 seconds
setTimeout(function() {
location.reload();
}, 30000);
</script>
</body>
</html>

View File

@@ -0,0 +1,247 @@
{% extends "admin/base.html" %}
{% block title %}Admin Dashboard - Moto Adventure{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="location.reload()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-primary text-uppercase mb-1">Total Users</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ total_users }}</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-success text-uppercase mb-1">Published Posts</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ published_posts }}</div>
</div>
<div class="col-auto">
<i class="fas fa-check fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-warning text-uppercase mb-1">Pending Posts</div>
<div class="h5 mb-0 fw-bold text-gray-800">
<a href="{{ url_for('admin.posts', status='pending') }}" class="text-decoration-none text-dark">
{{ pending_posts }}
</a>
</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-info text-uppercase mb-1">Active Users (30d)</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ active_users }}</div>
</div>
<div class="col-auto">
<i class="fas fa-user-check fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Views Statistics -->
<div class="row mb-4">
<div class="col-xl-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Views Today</h6>
</div>
<div class="card-body text-center">
<div class="h3 mb-0 text-gray-800">{{ views_today }}</div>
<small class="text-muted">Yesterday: {{ views_yesterday }}</small>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Views This Week</h6>
</div>
<div class="card-body text-center">
<div class="h3 mb-0 text-gray-800">{{ views_this_week }}</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Total Engagement</h6>
</div>
<div class="card-body">
<div class="small mb-1">Comments: <span class="float-end">{{ total_comments }}</span></div>
<div class="small mb-1">Likes: <span class="float-end">{{ total_likes }}</span></div>
</div>
</div>
</div>
</div>
<!-- Content Overview -->
<div class="row">
<!-- Recent Posts -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-primary">Recent Posts</h6>
<a href="{{ url_for('admin.posts') }}" class="btn btn-sm btn-primary">View All</a>
</div>
<div class="card-body">
{% if recent_posts %}
{% for post in recent_posts %}
<div class="d-flex align-items-center mb-3 border-bottom pb-2">
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="{{ url_for('admin.post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title[:50] }}{% if post.title|length > 50 %}...{% endif %}
</a>
</h6>
<small class="text-muted">
by {{ post.author.nickname }} • {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</div>
<div class="ms-2">
{% if post.published %}
<span class="badge bg-success">Published</span>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No recent posts</p>
{% endif %}
</div>
</div>
</div>
<!-- Most Viewed Posts -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Most Viewed Posts</h6>
</div>
<div class="card-body">
{% if most_viewed_posts %}
{% for post_id, title, view_count in most_viewed_posts %}
<div class="d-flex justify-content-between align-items-center mb-2 border-bottom pb-1">
<div class="flex-grow-1">
<a href="{{ url_for('admin.post_detail', post_id=post_id) }}" class="text-decoration-none">
{{ title[:40] }}{% if title|length > 40 %}...{% endif %}
</a>
</div>
<span class="badge bg-info">{{ view_count }} views</span>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No view data available</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Bottom Row -->
<div class="row">
<!-- Most Viewed Pages -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Most Viewed Pages</h6>
</div>
<div class="card-body">
{% if most_viewed_pages %}
{% for path, view_count in most_viewed_pages %}
<div class="d-flex justify-content-between align-items-center mb-2">
<code class="small">{{ path }}</code>
<span class="badge bg-secondary">{{ view_count }}</span>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No page view data available</p>
{% endif %}
</div>
</div>
</div>
<!-- Recent Users -->
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-primary">Recent Users</h6>
<a href="{{ url_for('admin.users') }}" class="btn btn-sm btn-primary">View All</a>
</div>
<div class="card-body">
{% if recent_users %}
{% for user in recent_users %}
<div class="d-flex align-items-center mb-3 border-bottom pb-2">
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}" class="text-decoration-none">
{{ user.nickname }}
</a>
</h6>
<small class="text-muted">{{ user.email }} • {{ user.created_at.strftime('%Y-%m-%d') }}</small>
</div>
<div class="ms-2">
{% if user.is_admin %}
<span class="badge bg-danger">Admin</span>
{% else %}
<span class="badge bg-primary">User</span>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No recent users</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,235 @@
{% extends "admin/base.html" %}
{% block title %}{{ post.title }} - Post Review{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Post Review</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{{ url_for('admin.posts') }}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Posts
</a>
</div>
<div class="btn-group">
{% if not post.published %}
<form method="POST" action="{{ url_for('admin.publish_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Publish this post?')">
<i class="fas fa-check"></i> Publish
</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('admin.unpublish_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-warning" onclick="return confirm('Unpublish this post?')">
<i class="fas fa-times"></i> Unpublish
</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('admin.delete_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Are you sure you want to delete this post? This action cannot be undone.')">
<i class="fas fa-trash"></i> Delete
</button>
</form>
</div>
</div>
</div>
<div class="row">
<!-- Post Details -->
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Post Content</h6>
</div>
<div class="card-body">
<h3>{{ post.title }}</h3>
{% if post.subtitle %}
<h5 class="text-muted mb-3">{{ post.subtitle }}</h5>
{% endif %}
<div class="mb-3">
<span class="badge bg-info">{{ post.get_difficulty_label() }}</span>
{% if post.published %}
<span class="badge bg-success">Published</span>
{% else %}
<span class="badge bg-warning text-dark">Pending Review</span>
{% endif %}
</div>
<div class="post-content">
{{ post.content|replace('\n', '<br>')|safe }}
</div>
</div>
</div>
<!-- Post Images -->
{% if post.images.count() > 0 %}
<div class="card mb-4">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Images ({{ post.images.count() }})</h6>
</div>
<div class="card-body">
<div class="row">
{% for image in post.images %}
<div class="col-md-4 mb-3">
<div class="card">
<img src="{{ image.get_url() }}" class="card-img-top" alt="{{ image.original_name }}"
style="height: 200px; object-fit: cover;">
<div class="card-body p-2">
<small class="text-muted">{{ image.original_name }}</small>
{% if image.is_cover %}
<span class="badge bg-primary badge-sm">Cover</span>
{% endif %}
{% if image.description %}
<p class="card-text small">{{ image.description }}</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- GPX Files -->
{% if post.gpx_files.count() > 0 %}
<div class="card mb-4">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">GPX Files ({{ post.gpx_files.count() }})</h6>
</div>
<div class="card-body">
{% for gpx_file in post.gpx_files %}
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<i class="fas fa-route text-primary"></i>
<strong>{{ gpx_file.original_name }}</strong>
<small class="text-muted">({{ "%.1f"|format(gpx_file.size / 1024) }} KB)</small>
</div>
<a href="{{ gpx_file.get_url() }}" class="btn btn-sm btn-outline-primary" download>
<i class="fas fa-download"></i> Download
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Post Metadata -->
<div class="card mb-4">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Post Information</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<td><strong>Author:</strong></td>
<td>
<a href="{{ url_for('admin.user_detail', user_id=post.author.id) }}">
{{ post.author.nickname }}
</a>
</td>
</tr>
<tr>
<td><strong>Created:</strong></td>
<td><small>{{ post.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small></td>
</tr>
<tr>
<td><strong>Updated:</strong></td>
<td><small>{{ post.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</small></td>
</tr>
<tr>
<td><strong>Difficulty:</strong></td>
<td>{{ post.get_difficulty_label() }} ({{ post.difficulty }}/5)</td>
</tr>
<tr>
<td><strong>Status:</strong></td>
<td>
{% if post.published %}
<span class="badge bg-success">Published</span>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Media Folder:</strong></td>
<td>
{% if post.media_folder %}
<code>{{ post.media_folder }}</code>
{% else %}
<em class="text-muted">None</em>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<!-- Post Statistics -->
<div class="card mb-4">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Statistics</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<td><strong>Comments:</strong></td>
<td>{{ post.comments.count() }}</td>
</tr>
<tr>
<td><strong>Likes:</strong></td>
<td>{{ post.get_like_count() }}</td>
</tr>
<tr>
<td><strong>Images:</strong></td>
<td>{{ post.images.count() }}</td>
</tr>
<tr>
<td><strong>GPX Files:</strong></td>
<td>{{ post.gpx_files.count() }}</td>
</tr>
<tr>
<td><strong>Views:</strong></td>
<td>{{ post.page_views|length if post.page_views else 0 }}</td>
</tr>
</table>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Quick Actions</h6>
</div>
<div class="card-body">
{% if post.published %}
<a href="{{ url_for('community.post_detail', post_id=post.id) }}"
class="btn btn-sm btn-primary w-100 mb-2" target="_blank">
<i class="fas fa-external-link-alt"></i> View on Site
</a>
{% endif %}
<a href="{{ url_for('admin.user_detail', user_id=post.author.id) }}"
class="btn btn-sm btn-info w-100">
<i class="fas fa-user"></i> View Author
</a>
</div>
</div>
</div>
</div>
<style>
.post-content {
line-height: 1.6;
white-space: pre-wrap;
}
.badge-sm {
font-size: 0.65em;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,176 @@
{% extends "admin/base.html" %}
{% block title %}Post Management - Admin{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Posts</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{{ url_for('admin.posts', status='all') }}"
class="btn btn-sm {{ 'btn-primary' if status == 'all' else 'btn-outline-secondary' }}">
All Posts
</a>
<a href="{{ url_for('admin.posts', status='published') }}"
class="btn btn-sm {{ 'btn-primary' if status == 'published' else 'btn-outline-secondary' }}">
Published
</a>
<a href="{{ url_for('admin.posts', status='pending') }}"
class="btn btn-sm {{ 'btn-primary' if status == 'pending' else 'btn-outline-secondary' }}">
Pending Review
</a>
</div>
</div>
</div>
{% if posts.items %}
<div class="card">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">
{% if status == 'pending' %}
Posts Pending Review ({{ posts.total }})
{% elif status == 'published' %}
Published Posts ({{ posts.total }})
{% else %}
All Posts ({{ posts.total }})
{% endif %}
</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 40%;">Title</th>
<th style="width: 15%;">Author</th>
<th style="width: 10%;">Status</th>
<th style="width: 10%;">Difficulty</th>
<th style="width: 15%;">Created</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody>
{% for post in posts.items %}
<tr>
<td>
<div>
<a href="{{ url_for('admin.post_detail', post_id=post.id) }}" class="text-decoration-none fw-bold">
{{ post.title }}
</a>
{% if post.subtitle %}
<br><small class="text-muted">{{ post.subtitle[:80] }}{% if post.subtitle|length > 80 %}...{% endif %}</small>
{% endif %}
</div>
</td>
<td>
<a href="{{ url_for('admin.user_detail', user_id=post.author.id) }}" class="text-decoration-none">
{{ post.author.nickname }}
</a>
</td>
<td>
{% if post.published %}
<span class="badge bg-success">Published</span>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
</td>
<td>
<span class="badge bg-info">{{ post.get_difficulty_label() }}</span>
</td>
<td>
<small>{{ post.created_at.strftime('%Y-%m-%d<br>%H:%M')|safe }}</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('admin.post_detail', post_id=post.id) }}"
class="btn btn-sm btn-outline-info" title="View Details">
<i class="fas fa-eye"></i>
</a>
{% if not post.published %}
<form method="POST" action="{{ url_for('admin.publish_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-success" title="Publish"
onclick="return confirm('Publish this post?')">
<i class="fas fa-check"></i>
</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('admin.unpublish_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-warning" title="Unpublish"
onclick="return confirm('Unpublish this post?')">
<i class="fas fa-times"></i>
</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('admin.delete_post', post_id=post.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger" title="Delete"
onclick="return confirm('Are you sure you want to delete this post? This action cannot be undone.')">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
{% if posts.pages > 1 %}
<nav aria-label="Posts pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.posts', page=posts.prev_num, status=status) }}">Previous</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages() %}
{% if page_num %}
{% if page_num != posts.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.posts', page=page_num, status=status) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.posts', page=posts.next_num, status=status) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No posts found</h5>
<p class="text-muted">
{% if status == 'pending' %}
There are no posts waiting for review.
{% elif status == 'published' %}
No posts have been published yet.
{% else %}
No posts have been created yet.
{% endif %}
</p>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,185 @@
{% extends "admin/base.html" %}
{% block title %}{{ user.nickname }} - User Details{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">User Details</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group mr-2">
<a href="{{ url_for('admin.users') }}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Users
</a>
</div>
</div>
</div>
<div class="row">
<!-- User Information -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">User Information</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<td><strong>Nickname:</strong></td>
<td>{{ user.nickname }}</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td><strong>Role:</strong></td>
<td>
{% if user.is_admin %}
<span class="badge badge-danger">Admin</span>
{% else %}
<span class="badge badge-primary">User</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Status:</strong></td>
<td>
{% if user.is_active %}
<span class="badge badge-success">Active</span>
{% else %}
<span class="badge badge-secondary">Inactive</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>Joined:</strong></td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
<tr>
<td><strong>Last Updated:</strong></td>
<td>{{ user.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
</table>
</div>
</div>
<!-- User Statistics -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Statistics</h6>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<td><strong>Total Posts:</strong></td>
<td>{{ user_posts|length }}</td>
</tr>
<tr>
<td><strong>Published Posts:</strong></td>
<td>{{ user_posts|selectattr('published')|list|length }}</td>
</tr>
<tr>
<td><strong>Pending Posts:</strong></td>
<td>{{ user_posts|rejectattr('published')|list|length }}</td>
</tr>
<tr>
<td><strong>Comments:</strong></td>
<td>{{ user_comments|length }}</td>
</tr>
<tr>
<td><strong>Likes Given:</strong></td>
<td>{{ user.likes.count() }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- User Content -->
<div class="col-lg-8">
<!-- User Posts -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Posts ({{ user_posts|length }})</h6>
</div>
<div class="card-body">
{% if user_posts %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for post in user_posts %}
<tr>
<td>
<a href="{{ url_for('admin.post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title[:50] }}{% if post.title|length > 50 %}...{% endif %}
</a>
</td>
<td>
{% if post.published %}
<span class="badge badge-success">Published</span>
{% else %}
<span class="badge badge-warning">Pending</span>
{% endif %}
</td>
<td>
<small>{{ post.created_at.strftime('%Y-%m-%d') }}</small>
</td>
<td>
<a href="{{ url_for('admin.post_detail', post_id=post.id) }}"
class="btn btn-sm btn-outline-info">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">This user has not created any posts yet.</p>
{% endif %}
</div>
</div>
<!-- Recent Comments -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Recent Comments ({{ user_comments|length }})</h6>
</div>
<div class="card-body">
{% if user_comments %}
{% for comment in user_comments[:10] %}
<div class="border-bottom mb-3 pb-3">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<p class="mb-1">{{ comment.content[:200] }}{% if comment.content|length > 200 %}...{% endif %}</p>
<small class="text-muted">
On post:
<a href="{{ url_for('admin.post_detail', post_id=comment.post.id) }}">
{{ comment.post.title }}
</a>
</small>
</div>
<small class="text-muted">{{ comment.created_at.strftime('%Y-%m-%d') }}</small>
</div>
</div>
{% endfor %}
{% if user_comments|length > 10 %}
<p class="text-muted text-center">... and {{ user_comments|length - 10 }} more comments</p>
{% endif %}
{% else %}
<p class="text-muted">This user has not made any comments yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,137 @@
{% extends "admin/base.html" %}
{% block title %}User Management - Admin{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Users</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="location.reload()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
{% if users.items %}
<div class="card">
<div class="card-header py-3">
<h6 class="m-0 fw-bold text-primary">Users ({{ users.total }})</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 20%;">User</th>
<th style="width: 25%;">Email</th>
<th style="width: 10%;">Role</th>
<th style="width: 15%;">Posts</th>
<th style="width: 10%;">Status</th>
<th style="width: 10%;">Joined</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users.items %}
<tr>
<td>
<div>
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}" class="text-decoration-none fw-bold">
{{ user.nickname }}
</a>
</div>
</td>
<td>
<small>{{ user.email }}</small>
</td>
<td>
{% if user.is_admin %}
<span class="badge bg-danger">Admin</span>
{% else %}
<span class="badge bg-primary">User</span>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ user.posts.count() }}</span>
{% if user.posts.filter_by(published=True).count() > 0 %}
<br><small class="text-success">({{ user.posts.filter_by(published=True).count() }} published)</small>
{% endif %}
</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td>
<small>{{ user.created_at.strftime('%Y-%m-%d') }}</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="btn btn-sm btn-outline-info" title="View Details">
<i class="fas fa-eye"></i>
</a>
{% if not user.is_admin or current_user.id != user.id %}
<button class="btn btn-sm btn-outline-warning" title="Toggle Status" disabled>
<i class="fas fa-toggle-on"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
{% if users.pages > 1 %}
<nav aria-label="Users pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if users.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num) }}">Previous</a>
</li>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link"></span>
</li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No users found</h5>
<p class="text-muted">No users have registered yet.</p>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -29,6 +29,9 @@
<a href="{{ url_for('community.new_post') }}" class="bg-gradient-to-r from-green-500 to-emerald-500 text-white px-4 py-2 rounded-lg hover:from-green-400 hover:to-emerald-400 transition transform hover:scale-105">
<i class="fas fa-plus mr-2"></i>Share Adventure
</a>
<a href="{{ url_for('community.profile') }}" class="text-white hover:text-blue-200 transition">
<i class="fas fa-user mr-1"></i>My Profile
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="text-white hover:text-yellow-200 transition">
<i class="fas fa-cog mr-1"></i>Admin
@@ -61,6 +64,9 @@
<a href="{{ url_for('community.new_post') }}" class="text-white block px-3 py-2 hover:bg-green-600 rounded">
<i class="fas fa-plus mr-2"></i>New Post
</a>
<a href="{{ url_for('community.profile') }}" class="text-white block px-3 py-2 hover:bg-blue-600 rounded">
<i class="fas fa-user mr-1"></i>My Profile
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="text-white block px-3 py-2 hover:bg-yellow-600 rounded">
<i class="fas fa-cog mr-1"></i>Admin

View File

@@ -0,0 +1,423 @@
{% extends "base.html" %}
{% block title %}Edit Adventure - {{ post.title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<!-- Form Section -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-edit"></i> Edit Your Adventure
</h4>
</div>
<div class="card-body">
<form id="editPostForm" method="POST" enctype="multipart/form-data" action="{{ url_for('community.edit_post', id=post.id) }}">
<!-- Title -->
<div class="mb-4">
<label for="title" class="form-label fw-bold">
<i class="fas fa-heading text-primary"></i> Adventure Title *
</label>
<input type="text" class="form-control form-control-lg" id="title" name="title"
value="{{ post.title }}" required maxlength="100"
placeholder="Enter your adventure title">
<div class="form-text">Make it catchy and descriptive!</div>
</div>
<!-- Subtitle -->
<div class="mb-4">
<label for="subtitle" class="form-label fw-bold">
<i class="fas fa-text-height text-info"></i> Subtitle
</label>
<input type="text" class="form-control" id="subtitle" name="subtitle"
value="{{ post.subtitle or '' }}" maxlength="200"
placeholder="A brief description of your adventure">
<div class="form-text">Optional - appears under the main title</div>
</div>
<!-- Difficulty Level -->
<div class="mb-4">
<label for="difficulty" class="form-label fw-bold">
<i class="fas fa-mountain text-warning"></i> Difficulty Level *
</label>
<select class="form-select" id="difficulty" name="difficulty" required>
<option value="">Select difficulty...</option>
<option value="1" {% if post.difficulty == 1 %}selected{% endif %}>⭐ Easy - Beginner friendly</option>
<option value="2" {% if post.difficulty == 2 %}selected{% endif %}>⭐⭐ Moderate - Some experience needed</option>
<option value="3" {% if post.difficulty == 3 %}selected{% endif %}>⭐⭐⭐ Challenging - Good skills required</option>
<option value="4" {% if post.difficulty == 4 %}selected{% endif %}>⭐⭐⭐⭐ Hard - Advanced riders only</option>
<option value="5" {% if post.difficulty == 5 %}selected{% endif %}>⭐⭐⭐⭐⭐ Expert - Extreme difficulty</option>
</select>
</div>
<!-- Current Cover Image -->
{% if post.images %}
{% set cover_image = post.images | selectattr('is_cover', 'equalto', True) | first %}
{% if cover_image %}
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-image text-success"></i> Current Cover Photo
</label>
<div class="current-cover-preview">
<img src="{{ cover_image.get_thumbnail_url() }}" alt="Current cover"
class="img-thumbnail" style="max-width: 200px;">
<small class="text-muted d-block mt-1">{{ cover_image.original_name }}</small>
</div>
</div>
{% endif %}
{% endif %}
<!-- Cover Photo Upload -->
<div class="mb-4">
<label for="cover_picture" class="form-label fw-bold">
<i class="fas fa-camera text-success"></i>
{% if post.images and post.images | selectattr('is_cover', 'equalto', True) | first %}
Replace Cover Photo
{% else %}
Cover Photo
{% endif %}
</label>
<input type="file" class="form-control" id="cover_picture" name="cover_picture"
accept="image/*" onchange="previewCoverImage(this)">
<div class="form-text">
Optional - Upload a new cover photo to replace the current one
</div>
<div id="cover_preview" class="mt-2"></div>
</div>
<!-- Adventure Story/Content -->
<div class="mb-4">
<label for="content" class="form-label fw-bold">
<i class="fas fa-book text-primary"></i> Your Adventure Story *
</label>
<textarea class="form-control" id="content" name="content" rows="8" required
placeholder="Tell us about your adventure... Where did you go? What did you see? Any challenges or highlights?">{{ post.content }}</textarea>
<div class="form-text">
<i class="fas fa-info-circle"></i>
You can use **bold text** and *italic text* in your story!
</div>
</div>
<!-- Current GPX File -->
{% if post.gpx_files %}
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-route text-info"></i> Current GPS Track
</label>
{% for gpx_file in post.gpx_files %}
<div class="current-gpx-file border rounded p-3 bg-light">
<div class="d-flex align-items-center">
<i class="fas fa-file-alt text-info me-2"></i>
<div>
<strong>{{ gpx_file.original_name }}</strong>
<small class="text-muted d-block">
Size: {{ "%.1f"|format(gpx_file.size / 1024) }} KB
• Uploaded: {{ gpx_file.created_at.strftime('%Y-%m-%d') }}
</small>
</div>
<a href="{{ url_for('community.serve_gpx', post_folder=post.media_folder, filename=gpx_file.filename) }}"
class="btn btn-sm btn-outline-primary ms-auto">
<i class="fas fa-download"></i> Download
</a>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- GPX File Upload -->
<div class="mb-4">
<label for="gpx_file" class="form-label fw-bold">
<i class="fas fa-route text-info"></i>
{% if post.gpx_files %}
Replace GPS Track File
{% else %}
GPS Track File (GPX)
{% endif %}
</label>
<input type="file" class="form-control" id="gpx_file" name="gpx_file"
accept=".gpx" onchange="validateGpxFile(this)">
<div class="form-text">
Optional - Upload a new GPX file to replace the current route
</div>
<div id="gpx_info" class="mt-2"></div>
</div>
<!-- Submit Buttons -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ url_for('community.profile') }}" class="btn btn-secondary me-md-2">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="button" class="btn btn-info me-md-2" onclick="previewPost()">
<i class="fas fa-eye"></i> Preview Changes
</button>
<button type="submit" class="btn btn-success">
<i class="fas fa-paper-plane"></i> Update & Resubmit for Review
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Info Panel -->
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-info-circle"></i> Editing Guidelines
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6><i class="fas fa-edit text-primary"></i> What happens after editing?</h6>
<p class="small">Your updated post will be resubmitted for admin review before being published again.</p>
</div>
<div class="mb-3">
<h6><i class="fas fa-image text-success"></i> Photo Guidelines</h6>
<ul class="small mb-0">
<li>Use high-quality images (JPEG, PNG)</li>
<li>Landscape orientation works best for cover photos</li>
<li>Maximum file size: 10MB</li>
</ul>
</div>
<div class="mb-3">
<h6><i class="fas fa-route text-info"></i> GPX File Tips</h6>
<ul class="small mb-0">
<li>Export from your GPS device or app</li>
<li>Should contain track points</li>
<li>Will be displayed on the community map</li>
</ul>
</div>
<div class="mb-3">
<h6><i class="fas fa-star text-warning"></i> Difficulty Levels</h6>
<div class="small">
<div><strong>Easy:</strong> Paved roads, good weather</div>
<div><strong>Moderate:</strong> Some gravel, hills</div>
<div><strong>Challenging:</strong> Off-road, technical</div>
<div><strong>Hard:</strong> Extreme conditions</div>
<div><strong>Expert:</strong> Dangerous, experts only</div>
</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Note:</strong> Updating your post will reset its status to "pending review."
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">
<i class="fas fa-eye"></i> Post Preview
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="previewContent">
<!-- Preview content will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close Preview</button>
<button type="button" class="btn btn-success" onclick="submitForm()">
<i class="fas fa-paper-plane"></i> Looks Good - Update Post
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script>
function previewCoverImage(input) {
const preview = document.getElementById('cover_preview');
preview.innerHTML = '';
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `
<div class="mt-2">
<img src="${e.target.result}" alt="Cover preview" class="img-thumbnail" style="max-width: 200px;">
<small class="text-success d-block mt-1">✓ New cover photo ready</small>
</div>
`;
};
reader.readAsDataURL(input.files[0]);
}
}
function validateGpxFile(input) {
const info = document.getElementById('gpx_info');
info.innerHTML = '';
if (input.files && input.files[0]) {
const file = input.files[0];
if (file.name.toLowerCase().endsWith('.gpx')) {
info.innerHTML = `
<div class="alert alert-success">
<i class="fas fa-check-circle"></i>
GPX file selected: ${file.name} (${(file.size / 1024).toFixed(1)} KB)
</div>
`;
} else {
info.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle"></i>
Please select a valid GPX file
</div>
`;
input.value = '';
}
}
}
function previewPost() {
// Get form data
const title = document.getElementById('title').value;
const subtitle = document.getElementById('subtitle').value;
const content = document.getElementById('content').value;
const difficulty = document.getElementById('difficulty').value;
// Get difficulty stars
const difficultyStars = '⭐'.repeat(difficulty);
const difficultyLabels = {
'1': 'Easy',
'2': 'Moderate',
'3': 'Challenging',
'4': 'Hard',
'5': 'Expert'
};
// Format content (simple markdown-like formatting)
const formattedContent = content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
// Generate preview HTML
const previewHTML = `
<div class="post-preview">
<!-- Hero Section -->
<div class="hero-section bg-primary text-white p-4 rounded mb-4">
<div class="container">
<h1 class="display-4 mb-2">${title || 'Your Adventure Title'}</h1>
${subtitle ? `<p class="lead mb-3">${subtitle}</p>` : ''}
<div class="d-flex align-items-center">
<span class="badge bg-warning text-dark me-3">
${difficultyStars} ${difficultyLabels[difficulty] || 'Select difficulty'}
</span>
<small>By {{ current_user.nickname }} • Updated today</small>
</div>
</div>
</div>
<!-- Content Section -->
<div class="container">
<div class="row">
<div class="col-lg-8">
<div class="adventure-content">
<h3>Adventure Story</h3>
<div class="content-text">
${formattedContent || '<em>No content provided yet...</em>'}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="adventure-info">
<h5>Adventure Details</h5>
<ul class="list-unstyled">
<li><strong>Difficulty:</strong> ${difficultyStars} ${difficultyLabels[difficulty] || 'Not set'}</li>
<li><strong>Status:</strong> <span class="badge bg-warning">Pending Review</span></li>
<li><strong>Last Updated:</strong> Today</li>
</ul>
</div>
</div>
</div>
</div>
</div>
`;
// Show preview in modal
document.getElementById('previewContent').innerHTML = previewHTML;
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
}
function submitForm() {
document.getElementById('editPostForm').submit();
}
// Form submission with AJAX
document.getElementById('editPostForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
const originalText = submitButton.innerHTML;
// Show loading state
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
submitButton.disabled = true;
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = `
<i class="fas fa-check-circle"></i> ${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
this.insertBefore(alert, this.firstChild);
// Redirect after delay
setTimeout(() => {
window.location.href = data.redirect_url;
}, 2000);
} else {
// Show error message
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show';
alert.innerHTML = `
<i class="fas fa-exclamation-triangle"></i> ${data.error}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
this.insertBefore(alert, this.firstChild);
}
})
.catch(error => {
console.error('Error:', error);
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show';
alert.innerHTML = `
<i class="fas fa-exclamation-triangle"></i> An error occurred while updating your post.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
this.insertBefore(alert, this.firstChild);
})
.finally(() => {
// Restore button state
submitButton.innerHTML = originalText;
submitButton.disabled = false;
});
});
</script>
{% endblock %}

View File

@@ -177,13 +177,13 @@
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
</svg>
{{ post.comments|length }}
{{ post.comments.count() }}
</span>
<span class="flex items-center">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path>
</svg>
{{ post.likes|length }}
{{ post.likes.count() }}
</span>
</div>

View File

@@ -130,7 +130,7 @@
</div>
<!-- Main Form -->
<form id="postForm" enctype="multipart/form-data" class="space-y-6">
<form id="adventure-form" method="POST" action="{{ url_for('community.new_post') }}" enctype="multipart/form-data" class="space-y-6">
<!-- Basic Information Section -->
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold text-white mb-6">📝 Basic Information</h2>
@@ -228,7 +228,11 @@
</div>
<!-- Submit -->
<div class="text-center">
<div class="text-center space-x-4">
<button type="button" onclick="previewPost()" class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-8 py-4 rounded-lg font-bold text-lg hover:from-blue-600 hover:to-indigo-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<i class="fas fa-eye mr-3"></i>
Preview Adventure
</button>
<button type="submit" class="bg-gradient-to-r from-orange-500 to-red-600 text-white px-12 py-4 rounded-lg font-bold text-lg hover:from-orange-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-lg">
<i class="fas fa-paper-plane mr-3"></i>
Share Adventure
@@ -242,6 +246,38 @@
document.addEventListener('DOMContentLoaded', function() {
let sectionCounter = 0;
// Populate form from URL parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('title')) {
document.getElementById('title').value = urlParams.get('title');
}
if (urlParams.get('subtitle')) {
document.getElementById('subtitle').value = urlParams.get('subtitle');
}
if (urlParams.get('difficulty')) {
document.getElementById('difficulty').value = urlParams.get('difficulty');
}
if (urlParams.get('cover_picture')) {
// Note: This would be the filename, but we can't pre-populate file inputs for security reasons
// We'll show a message instead
const coverUploadArea = document.querySelector('.cover-upload-area');
const coverContent = document.querySelector('.cover-upload-content');
coverContent.innerHTML = `
<div class="text-4xl mb-2">📸</div>
<p class="text-yellow-300 mb-2">Cover image suggested: ${urlParams.get('cover_picture')}</p>
<p class="text-white/80 mb-2">Click to upload this or another cover image</p>
<p class="text-sm text-white/60">Recommended: 1920x1080 pixels</p>
`;
}
if (urlParams.get('gpx_file')) {
// Similar note for GPX file
const gpxLabel = document.querySelector('label[for="gpx-file"]');
gpxLabel.innerHTML = `
<i class="fas fa-upload mr-2"></i>
Choose GPX File (suggested: ${urlParams.get('gpx_file')})
`;
}
// Cover picture upload
const coverUploadArea = document.querySelector('.cover-upload-area');
const coverInput = document.getElementById('cover_picture');
@@ -531,12 +567,8 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// Check if at least one section is saved
// Check if at least one section is saved or allow empty content for now
const savedSections = document.querySelectorAll('.content-section.saved');
if (savedSections.length === 0) {
alert('Please save at least one content section.');
return;
}
// Collect all form data
const formData = new FormData();
@@ -546,50 +578,245 @@ document.addEventListener('DOMContentLoaded', function() {
// Collect content from saved sections
let fullContent = '';
savedSections.forEach((section, index) => {
const highlights = Array.from(section.querySelectorAll('.saved-highlights .highlight-display'))
.map(span => span.textContent);
const text = section.querySelector('.saved-text').textContent;
if (highlights.length > 0) {
fullContent += highlights.map(h => `**${h}**`).join(' ') + '\n\n';
}
if (text) {
fullContent += text + '\n\n';
}
});
if (savedSections.length > 0) {
savedSections.forEach((section, index) => {
const highlights = Array.from(section.querySelectorAll('.saved-highlights .highlight-display'))
.map(span => span.textContent);
const text = section.querySelector('.saved-text').textContent;
if (highlights.length > 0) {
fullContent += highlights.map(h => `**${h}**`).join(' ') + '\n\n';
}
if (text) {
fullContent += text + '\n\n';
}
});
} else {
// If no sections saved, use a default message
fullContent = 'Adventure details will be added soon.';
}
formData.append('content', fullContent);
// Add cover picture if selected
const coverFile = document.getElementById('cover_picture').files[0];
if (coverFile) {
formData.append('cover_picture', coverFile);
}
// Add GPX file if selected
const gpxFile = document.getElementById('gpx-file').files[0];
if (gpxFile) {
formData.append('gpx_file', gpxFile);
}
// Add images (simplified - in a real implementation, you'd handle this properly)
const allImages = document.querySelectorAll('.saved-images img');
// Note: This is a simplified version. In a real implementation,
// you'd need to properly handle the image files
// Show loading state
const submitBtn = document.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-3"></i>Creating Adventure...';
submitBtn.disabled = true;
// Submit form
fetch('{{ url_for("community.new_post") }}', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect_url;
.then(response => {
if (response.redirected) {
// Follow redirect
window.location.href = response.url;
} else if (response.ok) {
// Check if it's JSON response
return response.text().then(text => {
try {
const data = JSON.parse(text);
if (data.success) {
window.location.href = data.redirect_url || '/community/';
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (e) {
// Not JSON, probably HTML redirect response
window.location.href = '/community/';
}
});
} else {
alert('Error creating post: ' + data.error);
throw new Error('Server error');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while creating your post.');
alert('An error occurred while creating your post. Please try again.');
// Restore button
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
});
});
// Preview function
function previewPost() {
// Get form data
const title = document.getElementById('title').value;
const subtitle = document.getElementById('subtitle').value;
const content = document.getElementById('content').value;
const difficulty = document.getElementById('difficulty').value;
// Get difficulty stars and labels
const difficultyStars = '⭐'.repeat(difficulty);
const difficultyLabels = {
'1': 'Easy - Beginner friendly',
'2': 'Moderate - Some experience needed',
'3': 'Challenging - Good skills required',
'4': 'Hard - Advanced riders only',
'5': 'Expert - Extreme difficulty'
};
// Format content (simple markdown-like formatting)
const formattedContent = content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
// Get cover image preview if available
const coverInput = document.getElementById('cover_picture');
let coverImageHtml = '';
if (coverInput.files && coverInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
showPreviewModal(title, subtitle, formattedContent, difficulty, difficultyStars, difficultyLabels, e.target.result);
};
reader.readAsDataURL(coverInput.files[0]);
} else {
showPreviewModal(title, subtitle, formattedContent, difficulty, difficultyStars, difficultyLabels, null);
}
}
function showPreviewModal(title, subtitle, formattedContent, difficulty, difficultyStars, difficultyLabels, coverImageSrc) {
// Create cover image section
let coverImageHtml = '';
if (coverImageSrc) {
coverImageHtml = `
<div class="position-relative mb-4">
<img src="${coverImageSrc}" alt="Cover preview" class="w-100 rounded" style="max-height: 300px; object-fit: cover;">
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-end" style="background: linear-gradient(transparent, rgba(0,0,0,0.6));">
<div class="p-4 text-white">
<h1 class="display-4 mb-2">${title || 'Your Adventure Title'}</h1>
${subtitle ? `<p class="lead mb-0">${subtitle}</p>` : ''}
</div>
</div>
</div>
`;
} else {
coverImageHtml = `
<div class="bg-primary text-white p-4 rounded mb-4">
<h1 class="display-4 mb-2">${title || 'Your Adventure Title'}</h1>
${subtitle ? `<p class="lead mb-0">${subtitle}</p>` : ''}
</div>
`;
}
// Generate preview HTML
const previewHTML = `
<div class="post-preview">
${coverImageHtml}
<div class="row">
<div class="col-lg-8">
<div class="adventure-content">
<h3><i class="fas fa-book text-primary"></i> Adventure Story</h3>
<div class="content-text mb-4">
${formattedContent || '<em class="text-muted">No content provided yet...</em>'}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Adventure Details</h5>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Difficulty:</strong><br>
<span class="badge bg-warning text-dark fs-6">
${difficultyStars} ${difficultyLabels[difficulty] || 'Not set'}
</span>
</div>
<div class="mb-3">
<strong>Status:</strong><br>
<span class="badge bg-warning">Pending Review</span>
</div>
<div class="mb-3">
<strong>Author:</strong><br>
{{ current_user.nickname }}
</div>
<div>
<strong>Created:</strong><br>
Today
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header bg-info text-white">
<h6 class="mb-0"><i class="fas fa-map-marked-alt"></i> Route Information</h6>
</div>
<div class="card-body">
${document.getElementById('gpx_file').files.length > 0 ?
'<div class="alert alert-success small"><i class="fas fa-check"></i> GPS track will be displayed on map</div>' :
'<div class="alert alert-info small"><i class="fas fa-info"></i> No GPS track uploaded</div>'
}
</div>
</div>
</div>
</div>
</div>
`;
// Show preview in modal
showModal('Adventure Preview', previewHTML);
}
function showModal(title, content) {
// Create modal if it doesn't exist
let modal = document.getElementById('previewModal');
if (!modal) {
modal = document.createElement('div');
modal.innerHTML = `
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">
<i class="fas fa-eye"></i> ${title}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="previewContent">${content}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Close Preview
</button>
<button type="button" class="btn btn-success" onclick="document.getElementById('createPostForm').submit()">
<i class="fas fa-paper-plane"></i> Looks Good - Share Adventure
</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
} else {
document.getElementById('previewContent').innerHTML = content;
document.getElementById('previewModalLabel').innerHTML = `<i class="fas fa-eye"></i> ${title}`;
}
// Show the modal
const bootstrapModal = new bootstrap.Modal(document.getElementById('previewModal'));
bootstrapModal.show();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,521 @@
{% 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 %}

View File

@@ -0,0 +1,941 @@
{% 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 %}

View File

@@ -0,0 +1,521 @@
{% 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 %}

View File

@@ -0,0 +1,297 @@
{% extends "base.html" %}
{% block title %}My Profile - {{ current_user.nickname }}{% endblock %}
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 pt-16">
<!-- Profile Header -->
<div class="relative overflow-hidden py-16">
<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">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-white/20">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center space-x-6">
<div class="w-20 h-20 bg-gradient-to-r from-orange-500 to-red-600 rounded-full flex items-center justify-center text-white font-bold text-3xl">
{{ current_user.nickname[0].upper() }}
</div>
<div>
<h1 class="text-3xl font-bold text-white mb-2">{{ current_user.nickname }}</h1>
<p class="text-blue-200 mb-1">
<i class="fas fa-envelope mr-2"></i>{{ current_user.email }}
</p>
<p class="text-blue-200">
<i class="fas fa-calendar mr-2"></i>Riding since {{ current_user.created_at.strftime('%B %Y') }}
</p>
</div>
</div>
<div>
<a href="{{ url_for('community.new_post') }}"
class="inline-flex items-center px-6 py-3 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-plus mr-2"></i>Share New Adventure
</a>
</div>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 -mt-8">
<!-- Adventure Statistics -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="bg-gradient-to-r from-green-600 to-emerald-600 p-6">
<div class="flex items-center text-white">
<i class="fas fa-check-circle text-3xl mr-4"></i>
<div>
<div class="text-3xl font-bold">{{ published_count }}</div>
<div class="text-green-100">Published Adventures</div>
</div>
</div>
</div>
<div class="p-4">
<p class="text-gray-600 text-sm">Visible to the community</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="bg-gradient-to-r from-yellow-600 to-orange-600 p-6">
<div class="flex items-center text-white">
<i class="fas fa-clock text-3xl mr-4"></i>
<div>
<div class="text-3xl font-bold">{{ pending_count }}</div>
<div class="text-yellow-100">Awaiting Review</div>
</div>
</div>
</div>
<div class="p-4">
<p class="text-gray-600 text-sm">Pending admin approval</p>
</div>
</div>
</div>
<!-- Adventures Collection Header -->
<div class="bg-white rounded-2xl shadow-xl p-6 mb-8">
<div class="flex items-center">
<i class="fas fa-mountain text-3xl text-blue-600 mr-4"></i>
<div>
<h2 class="text-2xl font-bold text-gray-900">My Adventure Collection</h2>
<p class="text-gray-600">Manage and share your motorcycle adventures</p>
</div>
</div>
</div>
<!-- Adventures Grid -->
{% if posts.items %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for post in posts.items %}
<div class="bg-white rounded-2xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl hover:scale-105">
<!-- Adventure Image -->
{% set cover_image = post.images | selectattr('is_cover', 'equalto', True) | first %}
{% if cover_image %}
<div class="relative">
<img src="{{ cover_image.get_thumbnail_url() }}"
alt="{{ post.title }}"
class="w-full h-48 object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
<div class="absolute top-3 right-3">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold
{{ 'bg-green-500 text-white' if post.published else 'bg-yellow-500 text-black' }}">
{% if post.published %}
<i class="fas fa-check-circle mr-1"></i>Live
{% else %}
<i class="fas fa-clock mr-1"></i>Review
{% endif %}
</span>
</div>
</div>
{% else %}
<div class="h-48 bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center relative">
<i class="fas fa-mountain text-white text-6xl opacity-50"></i>
<div class="absolute top-3 right-3">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold
{{ 'bg-green-500 text-white' if post.published else 'bg-yellow-500 text-black' }}">
{% if post.published %}
<i class="fas fa-check-circle mr-1"></i>Live
{% else %}
<i class="fas fa-clock mr-1"></i>Review
{% endif %}
</span>
</div>
</div>
{% endif %}
<div class="p-6">
<!-- Title and Subtitle -->
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ post.title }}</h3>
{% if post.subtitle %}
<p class="text-gray-600 text-sm mb-3">{{ post.subtitle }}</p>
{% endif %}
<!-- Difficulty Badge -->
<div class="mb-4">
<span class="inline-flex items-center px-3 py-1 rounded-full bg-gradient-to-r from-yellow-500 to-orange-500 text-white text-sm font-semibold">
{% for i in range(post.difficulty) %}
<i class="fas fa-star mr-1"></i>
{% endfor %}
{{ post.get_difficulty_label() }}
</span>
</div>
<!-- Content Preview -->
<p class="text-gray-600 text-sm mb-4 h-12 overflow-hidden">
{{ (post.content[:100] + '...') if post.content|length > 100 else post.content }}
</p>
<!-- Adventure Metadata -->
<div class="bg-gray-50 rounded-lg p-4 mb-4">
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<i class="fas fa-calendar text-blue-500 mb-1"></i>
<div class="text-sm font-semibold">{{ post.created_at.strftime('%m/%d') }}</div>
<div class="text-xs text-gray-500">Created</div>
</div>
<div>
<i class="fas fa-images text-green-500 mb-1"></i>
<div class="text-sm font-semibold">{{ post.images.count() }}</div>
<div class="text-xs text-gray-500">Photos</div>
</div>
<div>
<i class="fas fa-route text-orange-500 mb-1"></i>
<div class="text-sm font-semibold">{{ post.gpx_files.count() }}</div>
<div class="text-xs text-gray-500">Routes</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="space-y-2">
{% if post.published %}
<a href="{{ url_for('community.post_detail', id=post.id) }}"
class="block w-full text-center px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-all duration-200">
<i class="fas fa-eye mr-2"></i>View Live
</a>
{% endif %}
<div class="grid grid-cols-2 gap-2">
<a href="{{ url_for('community.edit_post', id=post.id) }}"
class="text-center px-3 py-2 bg-yellow-500 text-white font-semibold rounded-lg hover:bg-yellow-600 transition-all duration-200">
<i class="fas fa-edit mr-1"></i>Edit
</a>
<button type="button"
class="px-3 py-2 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition-all duration-200"
data-post-id="{{ post.id }}"
data-post-title="{{ post.title }}"
onclick="confirmDelete(this)">
<i class="fas fa-trash mr-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<!-- No Adventures State -->
<div class="bg-white rounded-2xl shadow-xl p-12 text-center">
<i class="fas fa-mountain text-6xl text-gray-400 mb-6"></i>
<h3 class="text-2xl font-bold text-gray-900 mb-4">Your Adventure Journey Awaits!</h3>
<p class="text-gray-600 mb-8 max-w-md mx-auto">
You haven't shared any motorcycle adventures yet. Every great ride has a story worth telling!
</p>
<a href="{{ url_for('community.new_post') }}"
class="inline-flex items-center px-8 py-4 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-plus mr-2"></i>Share Your First Adventure
</a>
<div class="mt-6">
<p class="text-sm text-gray-500">
<i class="fas fa-lightbulb mr-1"></i>
Share photos, GPS tracks, and stories of your rides to inspire the community
</p>
</div>
</div>
{% endif %}
<!-- Pagination -->
{% if posts.pages > 1 %}
<div class="flex justify-center mt-8">
<div class="bg-white rounded-lg shadow-lg px-6 py-4">
<div class="flex items-center space-x-4">
{% if posts.has_prev %}
<a href="{{ url_for('community.profile', page=posts.prev_num) }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200">
<i class="fas fa-chevron-left mr-1"></i>Previous
</a>
{% endif %}
<span class="text-gray-600">
Page {{ posts.page }} of {{ posts.pages }}
</span>
{% if posts.has_next %}
<a href="{{ url_for('community.profile', page=posts.next_num) }}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all duration-200">
Next<i class="fas fa-chevron-right ml-1"></i>
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-2xl p-8 max-w-md mx-4">
<div class="text-center">
<i class="fas fa-exclamation-triangle text-red-500 text-4xl mb-4"></i>
<h3 class="text-xl font-bold text-gray-900 mb-4">Delete Adventure</h3>
<p class="text-gray-600 mb-6">
Are you sure you want to delete "<span id="deletePostTitle" class="font-semibold"></span>"?
This action cannot be undone.
</p>
<div class="flex space-x-4">
<button onclick="closeDeleteModal()"
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-all duration-200">
Cancel
</button>
<form id="deleteForm" method="POST" class="flex-1">
<button type="submit"
class="w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-all duration-200">
Delete
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function confirmDelete(button) {
const postId = button.getAttribute('data-post-id');
const postTitle = button.getAttribute('data-post-title');
document.getElementById('deletePostTitle').textContent = postTitle;
document.getElementById('deleteForm').action = `/community/delete-post/${postId}`;
document.getElementById('deleteModal').classList.remove('hidden');
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
// Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeDeleteModal();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,312 @@
{% extends "base.html" %}
{% block title %}My Profile - {{ current_user.nickname }}{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<!-- Header Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="mb-3">
<i class="fas fa-user-circle text-primary"></i>
{{ current_user.nickname }}'s Profile
</h1>
<p class="text-muted mb-0">
<i class="fas fa-envelope"></i> {{ current_user.email }}
<i class="fas fa-calendar"></i> Member since {{ current_user.created_at.strftime('%B %Y') }}
</p>
</div>
<div class="col-md-4 text-md-end">
<a href="{{ url_for('community.new_post') }}" class="btn btn-primary btn-lg">
<i class="fas fa-plus"></i> Create New Adventure
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Stats Section -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card text-center border-success">
<div class="card-body">
<i class="fas fa-check-circle fa-3x text-success mb-3"></i>
<h3 class="text-success">{{ published_count }}</h3>
<p class="text-muted mb-0">Published Adventures</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card text-center border-warning">
<div class="card-body">
<i class="fas fa-clock fa-3x text-warning mb-3"></i>
<h3 class="text-warning">{{ pending_count }}</h3>
<p class="text-muted mb-0">Pending Review</p>
</div>
</div>
</div>
</div>
<!-- Posts Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-dark text-white">
<h4 class="mb-0">
<i class="fas fa-list"></i> My Adventures
</h4>
</div>
<div class="card-body">
{% if posts.items %}
<div class="row">
{% for post in posts.items %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card h-100 {{ 'border-success' if post.published else 'border-warning' }}">
<!-- Cover Image -->
{% set cover_image = post.images | selectattr('is_cover', 'equalto', True) | first %}
{% if cover_image %}
<div class="position-relative">
<img src="{{ cover_image.get_thumbnail_url() }}"
alt="{{ post.title }}"
class="card-img-top"
style="height: 200px; object-fit: cover;">
<div class="position-absolute top-0 end-0 m-2">
<span class="badge {{ 'bg-success' if post.published else 'bg-warning text-dark' }}">
{{ 'Published' if post.published else 'Pending Review' }}
</span>
</div>
</div>
{% else %}
<div class="card-img-top bg-gradient-primary d-flex align-items-center justify-content-center text-white" style="height: 200px;">
<div class="text-center">
<i class="fas fa-mountain fa-3x mb-2"></i>
<div class="position-absolute top-0 end-0 m-2">
<span class="badge {{ 'bg-success' if post.published else 'bg-warning text-dark' }}">
{{ 'Published' if post.published else 'Pending Review' }}
</span>
</div>
</div>
</div>
{% endif %}
<div class="card-body d-flex flex-column">
<!-- Title and Subtitle -->
<h5 class="card-title">{{ post.title }}</h5>
{% if post.subtitle %}
<p class="card-text text-muted small">{{ post.subtitle }}</p>
{% endif %}
<!-- Difficulty -->
<div class="mb-2">
<span class="badge bg-warning text-dark">
{{ '⭐' * post.difficulty }}
{% if post.difficulty == 1 %}Easy{% elif post.difficulty == 2 %}Moderate{% elif post.difficulty == 3 %}Challenging{% elif post.difficulty == 4 %}Hard{% else %}Expert{% endif %}
</span>
</div>
<!-- Content Preview -->
<p class="card-text flex-grow-1">
{{ (post.content[:100] + '...') if post.content|length > 100 else post.content }}
</p>
<!-- Meta Information -->
<div class="text-muted small mb-3">
<div>
<i class="fas fa-calendar"></i>
Created: {{ post.created_at.strftime('%Y-%m-%d') }}
</div>
{% if post.updated_at and post.updated_at != post.created_at %}
<div>
<i class="fas fa-edit"></i>
Updated: {{ post.updated_at.strftime('%Y-%m-%d') }}
</div>
{% endif %}
<div>
<i class="fas fa-images"></i>
{{ post.images.count() }} images
<i class="fas fa-route"></i>
{{ post.gpx_files.count() }} GPS tracks
</div>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-2">
<div class="btn-group" role="group">
{% if post.published %}
<a href="{{ url_for('community.post_detail', id=post.id) }}"
class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye"></i> View
</a>
{% endif %}
<a href="{{ url_for('community.edit_post', id=post.id) }}"
class="btn btn-outline-warning btn-sm">
<i class="fas fa-edit"></i> Edit
</a>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="confirmDelete({{ post.id }}, '{{ post.title|replace("'", "\\'") }}')">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if posts.pages > 1 %}
<nav aria-label="Posts pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('community.profile', page=posts.prev_num) }}">
<i class="fas fa-chevron-left"></i> Previous
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages() %}
{% if page_num %}
{% if page_num != posts.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('community.profile', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('community.profile', page=posts.next_num) }}">
Next <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- No Posts State -->
<div class="text-center py-5">
<i class="fas fa-mountain fa-5x text-muted mb-4"></i>
<h3 class="text-muted">No Adventures Yet</h3>
<p class="text-muted mb-4">You haven't shared any motorcycle adventures yet. Start your journey!</p>
<a href="{{ url_for('community.new_post') }}" class="btn btn-primary btn-lg">
<i class="fas fa-plus"></i> Create Your First Adventure
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">
<i class="fas fa-exclamation-triangle"></i> Confirm Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the adventure:</p>
<h6 id="deletePostTitle" class="text-danger"></h6>
<p class="text-muted small mb-0">
<i class="fas fa-exclamation-circle"></i>
This action cannot be undone. All images and GPS tracks will also be deleted.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash"></i> Yes, Delete Adventure
</button>
</div>
</div>
</div>
</div>
<script>
let deletePostId = null;
function confirmDelete(postId, postTitle) {
deletePostId = postId;
document.getElementById('deletePostTitle').textContent = postTitle;
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
document.getElementById('confirmDeleteBtn').addEventListener('click', function() {
if (deletePostId) {
const btn = this;
const originalText = btn.innerHTML;
// Show loading state
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
btn.disabled = true;
// Send delete request
fetch(`/community/delete-post/${deletePostId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
modal.hide();
// Show success message and reload page
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = `
<i class="fas fa-check-circle"></i> ${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container-fluid').insertBefore(alert, document.querySelector('.container-fluid').firstChild);
// Reload page after delay
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
throw new Error(data.error || 'Failed to delete post');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the post: ' + error.message);
})
.finally(() => {
// Restore button state
btn.innerHTML = originalText;
btn.disabled = false;
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Simple Post Creation Test{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>Simple Post Creation Test</h3>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('community.new_post') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">Title *</label>
<input type="text" class="form-control" id="title" name="title" required
value="{{ request.args.get('title', '') }}">
</div>
<div class="mb-3">
<label for="subtitle" class="form-label">Subtitle</label>
<input type="text" class="form-control" id="subtitle" name="subtitle"
value="{{ request.args.get('subtitle', '') }}">
</div>
<div class="mb-3">
<label for="difficulty" class="form-label">Difficulty *</label>
<select class="form-control" id="difficulty" name="difficulty" required>
<option value="">Select difficulty...</option>
<option value="1" {% if request.args.get('difficulty') == '1' %}selected{% endif %}>🟢 Easy</option>
<option value="2" {% if request.args.get('difficulty') == '2' %}selected{% endif %}>🟡 Moderate</option>
<option value="3" {% if request.args.get('difficulty') == '3' %}selected{% endif %}>🟠 Challenging</option>
<option value="4" {% if request.args.get('difficulty') == '4' %}selected{% endif %}>🔴 Difficult</option>
<option value="5" {% if request.args.get('difficulty') == '5' %}selected{% endif %}>🟣 Expert</option>
</select>
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea class="form-control" id="content" name="content" rows="4"
placeholder="Adventure details (optional for testing)">{{ request.args.get('content', 'Adventure details will be added later.') }}</textarea>
</div>
<div class="mb-3">
<label for="cover_picture" class="form-label">Cover Picture</label>
<input type="file" class="form-control" id="cover_picture" name="cover_picture" accept="image/*">
{% if request.args.get('cover_picture') %}
<small class="text-muted">Suggested: {{ request.args.get('cover_picture') }}</small>
{% endif %}
</div>
<div class="mb-3">
<label for="gpx_file" class="form-label">GPX File</label>
<input type="file" class="form-control" id="gpx_file" name="gpx_file" accept=".gpx">
{% if request.args.get('gpx_file') %}
<small class="text-muted">Suggested: {{ request.args.get('gpx_file') }}</small>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">Create Post</button>
<a href="{{ url_for('community.index') }}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}