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