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:
204
app/templates/admin/analytics.html
Normal file
204
app/templates/admin/analytics.html
Normal 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 %}
|
||||
233
app/templates/admin/base.html
Normal file
233
app/templates/admin/base.html
Normal 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>
|
||||
247
app/templates/admin/dashboard.html
Normal file
247
app/templates/admin/dashboard.html
Normal 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 %}
|
||||
235
app/templates/admin/post_detail.html
Normal file
235
app/templates/admin/post_detail.html
Normal 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 %}
|
||||
176
app/templates/admin/posts.html
Normal file
176
app/templates/admin/posts.html
Normal 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 %}
|
||||
185
app/templates/admin/user_detail.html
Normal file
185
app/templates/admin/user_detail.html
Normal 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 %}
|
||||
137
app/templates/admin/users.html
Normal file
137
app/templates/admin/users.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user