Initial commit
This commit is contained in:
commit
315e731234
27 changed files with 3403 additions and 0 deletions
76
templates/admin/dashboard.html
Normal file
76
templates/admin/dashboard.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin Dashboard - Your Name{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<div>
|
||||
<a href="{{ url_for('admin_new_post') }}" class="btn btn-primary">New Post</a>
|
||||
<a href="{{ url_for('admin_logout') }}" class="btn btn-outline-secondary">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Blog Posts ({{ posts|length }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if posts %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for post in posts %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ post.title }}</strong><br>
|
||||
<small class="text-muted">{{ post.excerpt[:100] }}...</small>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ post.category }}</span></td>
|
||||
<td>{{ post.date }}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{{ url_for('blog_post', post_id=post.id) }}"
|
||||
class="btn btn-outline-primary" target="_blank">View</a>
|
||||
<a href="{{ url_for('admin_edit_post', post_id=post.id) }}"
|
||||
class="btn btn-outline-warning">Edit</a>
|
||||
<form method="POST" action="{{ url_for('admin_delete_post', post_id=post.id) }}"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this post?')">
|
||||
<button type="submit" class="btn btn-outline-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<p class="text-muted">No blog posts yet.</p>
|
||||
<a href="{{ url_for('admin_new_post') }}" class="btn btn-primary">Create Your First Post</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
98
templates/admin/edit_post.html
Normal file
98
templates/admin/edit_post.html
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ title }} - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>{{ title }}</h1>
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-secondary">Back to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.title.label(class="form-label") }}
|
||||
{{ form.title(class="form-control") }}
|
||||
{% if form.title.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.title.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form.category.label(class="form-label") }}
|
||||
{{ form.category(class="form-select") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.excerpt.label(class="form-label") }}
|
||||
{{ form.excerpt(class="form-control", rows="3", placeholder="Auto-generated if left blank") }}
|
||||
<div class="form-text">Brief description shown in blog listings</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.content.label(class="form-label") }}
|
||||
{{ form.content(class="form-control") }}
|
||||
{% if form.content.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.content.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
{% if post %}
|
||||
<a href="{{ url_for('blog_post', post_id=post.id) }}"
|
||||
class="btn btn-outline-info" target="_blank">Preview</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Writing Tips</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="small">
|
||||
<li>Use clear, descriptive titles</li>
|
||||
<li>Break up long paragraphs</li>
|
||||
<li>Include code examples when relevant</li>
|
||||
<li>Add personal insights and experiences</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Markdown Support</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">You can use basic HTML in your posts:</p>
|
||||
<code><strong>bold</strong></code><br>
|
||||
<code><em>italic</em></code><br>
|
||||
<code><code>code</code></code><br>
|
||||
<code><pre>code block</pre></code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
templates/admin/login.html
Normal file
45
templates/admin/login.html
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin Login - Your Name{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Admin Login</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% 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">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.username.label(class="form-label") }}
|
||||
{{ form.username(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
216
templates/admin/traffic.html
Normal file
216
templates/admin/traffic.html
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Traffic Analytics - Admin</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.stat-number { font-size: 2em; font-weight: bold; color: #2563eb; }
|
||||
.stat-label { color: #666; margin-top: 5px; }
|
||||
.chart-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; }
|
||||
.table-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
th { background: #f8f9fa; font-weight: 600; }
|
||||
h1 { color: #333; margin-bottom: 30px; }
|
||||
h2 { color: #333; margin-bottom: 15px; }
|
||||
.controls { margin-bottom: 20px; }
|
||||
.controls select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
.real-time-btn { background: #10b981; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.real-time-btn:hover { background: #059669; }
|
||||
.activity-item { padding: 8px; margin: 4px 0; background: #f8f9fa; border-radius: 4px; font-size: 0.9em; }
|
||||
.activity-time { color: #666; font-size: 0.8em; }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
@media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||
<h1>Traffic Analytics Dashboard</h1>
|
||||
<div class="controls">
|
||||
<select id="dateRange">
|
||||
<option value="7" {% if days == 7 %}selected{% endif %}>Last 7 days</option>
|
||||
<option value="30" {% if days == 30 %}selected{% endif %}>Last 30 days</option>
|
||||
<option value="90" {% if days == 90 %}selected{% endif %}>Last 90 days</option>
|
||||
</select>
|
||||
<button id="realTimeBtn" class="real-time-btn">
|
||||
Real-time: <span id="activeUsers">0</span> active
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ "{:,}".format(stats.total_views) }}</div>
|
||||
<div class="stat-label">Total Page Views</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ "{:,}".format(stats.unique_visitors) }}</div>
|
||||
<div class="stat-label">Unique Visitors</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.avg_response_time }}ms</div>
|
||||
<div class="stat-label">Avg Response Time</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.bounce_rate }}%</div>
|
||||
<div class="stat-label">Bounce Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Daily Traffic Chart -->
|
||||
<div class="chart-container">
|
||||
<h2>Daily Traffic (Last {{ days }} Days)</h2>
|
||||
<canvas id="dailyTrafficChart" width="400" height="100"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Tables -->
|
||||
<div class="grid-2">
|
||||
<div class="table-container">
|
||||
<h2>Top Pages</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for page, views in top_pages %}
|
||||
<tr>
|
||||
<td>{{ page }}</td>
|
||||
<td>{{ views }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top Referrers</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Referrer</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for referrer, views in top_referrers %}
|
||||
<tr>
|
||||
<td>{{ referrer[:50] }}{% if referrer|length > 50 %}...{% endif %}</td>
|
||||
<td>{{ views }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="table-container">
|
||||
<h2>Recent Activity (Last 24 Hours)</h2>
|
||||
<div id="recentActivity" style="max-height: 300px; overflow-y: auto;">
|
||||
{% for activity in recent_activity %}
|
||||
<div class="activity-item">
|
||||
<strong>{{ activity.path }}</strong>
|
||||
{% if activity.referrer %}
|
||||
from {{ activity.referrer[:30] }}{% if activity.referrer|length > 30 %}...{% endif %}
|
||||
{% endif %}
|
||||
<div class="activity-time">{{ activity.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} - {{ activity.ip_address[:8] }}...</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Chart.js configurations - Fixed tojsonfilter to tojson
|
||||
const dailyData = {{ daily_views | tojson | safe }};
|
||||
|
||||
// Daily Traffic Chart
|
||||
const ctx = document.getElementById('dailyTrafficChart').getContext('2d');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dailyData.map(d => d.date),
|
||||
datasets: [{
|
||||
label: 'Page Views',
|
||||
data: dailyData.map(d => d.views),
|
||||
borderColor: 'rgb(37, 99, 235)',
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
||||
tension: 0.1,
|
||||
fill: true
|
||||
}, {
|
||||
label: 'Unique Visitors',
|
||||
data: dailyData.map(d => d.unique_visitors),
|
||||
borderColor: 'rgb(16, 185, 129)',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
tension: 0.1,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time updates
|
||||
function updateRealTime() {
|
||||
fetch('/admin/traffic/api/realtime')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('activeUsers').textContent = data.active_users;
|
||||
|
||||
const activityDiv = document.getElementById('recentActivity');
|
||||
if (data.recent_views && data.recent_views.length > 0) {
|
||||
activityDiv.innerHTML = data.recent_views.map(view => `
|
||||
<div class="activity-item">
|
||||
<strong>${view.path}</strong>
|
||||
<div class="activity-time">
|
||||
${new Date(view.timestamp).toLocaleString()} - ${view.ip_address}
|
||||
${view.country ? ' (' + view.country + ')' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Real-time update failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Update every 30 seconds
|
||||
setInterval(updateRealTime, 30000);
|
||||
updateRealTime(); // Initial load
|
||||
|
||||
// Date range selector
|
||||
document.getElementById('dateRange').addEventListener('change', function() {
|
||||
window.location.href = `?days=${this.value}`;
|
||||
});
|
||||
|
||||
// Real-time button click
|
||||
document.getElementById('realTimeBtn').addEventListener('click', function() {
|
||||
updateRealTime();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue