253 lines
15 KiB
HTML
253 lines
15 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block title %}URL Analytics - {{ url.code }} - BZOD{% endblock %}
|
|
|
|
{% block active_urls %}active{% endblock %}
|
|
|
|
{% block header_title %}URL Analytics: /{{ url.code }}{% endblock %}
|
|
|
|
{% block header_actions %}
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<a href="/admin/analytics/url/{{ url.id }}/export/csv?date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
|
📥 Export CSV
|
|
</a>
|
|
<a href="/admin/analytics/url/{{ url.id }}/export/json?date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
|
📥 Export JSON
|
|
</a>
|
|
<a href="/admin/urls" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
|
Back to URLs
|
|
</a>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Date Filter Form -->
|
|
<div class="card" style="margin-bottom: 2rem;">
|
|
<form method="GET" action="/admin/analytics/url/{{ url.id }}" style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end;">
|
|
<div class="form-group" style="margin: 0; flex: 1; min-width: 150px;">
|
|
<label for="date_from" style="margin-bottom: 0.25rem; font-size: 0.85rem;">Date From</label>
|
|
<input type="date" id="date_from" name="date_from" class="form-input" value="{{ date_from.as_deref().unwrap_or("") }}" style="padding: 0.4rem 0.6rem;">
|
|
</div>
|
|
<div class="form-group" style="margin: 0; flex: 1; min-width: 150px;">
|
|
<label for="date_to" style="margin-bottom: 0.25rem; font-size: 0.85rem;">Date To</label>
|
|
<input type="date" id="date_to" name="date_to" class="form-input" value="{{ date_to.as_deref().unwrap_or("") }}" style="padding: 0.4rem 0.6rem;">
|
|
</div>
|
|
<button type="submit" class="btn" style="padding: 0.45rem 1.25rem; font-size: 0.9rem;">Apply Filters</button>
|
|
<a href="/admin/analytics/url/{{ url.id }}" class="btn btn-secondary" style="padding: 0.45rem 1.25rem; font-size: 0.9rem; text-decoration: none; display: inline-flex; align-items: center; justify-content: center;">Clear</a>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- URL Details Card -->
|
|
<div class="card" style="margin-bottom: 2rem;">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1rem; color: var(--text-primary); display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
|
Link Details
|
|
</h3>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
|
|
<div>
|
|
<span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.25rem;">Destination URL</span>
|
|
<a href="{{ url.destination }}" target="_blank" style="color: var(--accent-color); text-decoration: none; font-weight: 600; word-break: break-all;">
|
|
{{ url.destination }}
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.25rem;">Title</span>
|
|
<span style="font-weight: 500;">{{ url.title.as_deref().unwrap_or("No Title") }}</span>
|
|
</div>
|
|
<div>
|
|
<span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.25rem;">Created Date</span>
|
|
<span>{{ url.created_at[0..10] }} {{ url.created_at[11..16] }}</span>
|
|
</div>
|
|
<div>
|
|
<span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.25rem;">Tags</span>
|
|
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap; margin-top: 0.25rem;">
|
|
{% if url.tags.is_empty() %}
|
|
<span style="color: var(--text-muted); font-size: 0.85rem;">No tags</span>
|
|
{% else %}
|
|
{% for t in url.tags %}
|
|
<span class="badge" style="background-color: rgba(255,255,255,0.05); color: var(--text-secondary); border: 1px solid var(--border-color);">{{ t }}</span>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if let Some(desc) = url.description.as_deref() %}
|
|
{% if !desc.is_empty() %}
|
|
<div style="margin-top: 1.25rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
|
<span style="font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 0.25rem;">Description</span>
|
|
<p style="color: var(--text-secondary); font-size: 0.95rem;">{{ desc }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Overview Stats Cards -->
|
|
<div class="grid-stats" style="margin-bottom: 2rem;">
|
|
<div class="card stat-card">
|
|
<span class="stat-label">Total Clicks</span>
|
|
<span class="stat-val" style="color: #60a5fa;">{{ total_clicks }}</span>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<span class="stat-label">Unique Visitors</span>
|
|
<span class="stat-val" style="color: #c084fc;">{{ unique_visitors }}</span>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<span class="stat-label">QR Code scans</span>
|
|
<span class="stat-val" style="color: #f472b6;">{{ qr_scans }}</span>
|
|
</div>
|
|
<div class="card stat-card">
|
|
<span class="stat-label">Direct Clicks</span>
|
|
<span class="stat-val" style="color: #34d399;">{{ direct_clicks }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Traffic Charts (Timeline) -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; color: var(--text-primary); display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
Daily Click Traffic (Last 30 Days)
|
|
</h3>
|
|
<div style="background-color: rgba(15, 23, 42, 0.4); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255, 255, 255, 0.03);">
|
|
{{ traffic_chart|safe }}
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; color: var(--text-primary); display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
Monthly Click Traffic
|
|
</h3>
|
|
<div style="background-color: rgba(15, 23, 42, 0.4); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255, 255, 255, 0.03);">
|
|
{{ monthly_chart|safe }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Geographic, Referrers, and Browsers Analysis -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
|
<!-- Countries -->
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
Geographic Analysis (Top Countries)
|
|
</h3>
|
|
<div style="background-color: rgba(15, 23, 42, 0.4); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255, 255, 255, 0.03);">
|
|
{{ countries_chart|safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Referrers -->
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
Referrer Channels (Top Referrers)
|
|
</h3>
|
|
<div style="background-color: rgba(15, 23, 42, 0.4); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255, 255, 255, 0.03);">
|
|
{{ referrers_chart|safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Browsers -->
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/></svg>
|
|
Browser breakdown (Top Browsers)
|
|
</h3>
|
|
<div style="background-color: rgba(15, 23, 42, 0.4); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255, 255, 255, 0.03);">
|
|
{{ browsers_chart|safe }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Visitor Activity Log -->
|
|
<div class="card" style="margin-top: 2rem; padding: 0; overflow: hidden;">
|
|
<div style="padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border-color);">
|
|
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem; margin: 0;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
Visitor Activity Log
|
|
</h3>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Sr</th>
|
|
<th>Timestamp</th>
|
|
<th>IP Address</th>
|
|
<th>Country</th>
|
|
<th>Referrer</th>
|
|
<th>Browser</th>
|
|
<th>User-Agent</th>
|
|
<th>UTM Source</th>
|
|
<th>UTM Campaign</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if visits.is_empty() %}
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
|
No visitor activity available.
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
{% for entry in visits %}
|
|
<tr>
|
|
<td>{{ entry.sr }}</td>
|
|
<td style="font-family: monospace; font-size: 0.85rem; white-space: nowrap;">{{ entry.timestamp }}</td>
|
|
<td style="font-family: monospace; font-size: 0.85rem;">{{ entry.ip_address }}</td>
|
|
<td>
|
|
<span class="badge badge-healthy" style="text-transform: none;">
|
|
{{ entry.country }}
|
|
</span>
|
|
</td>
|
|
<td>{{ entry.referrer }}</td>
|
|
<td>{{ entry.browser }}</td>
|
|
<td style="max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; color: var(--text-secondary);" title="{{ entry.user_agent }}">
|
|
{{ entry.user_agent }}
|
|
</td>
|
|
<td>{{ entry.utm_source }}</td>
|
|
<td>{{ entry.utm_campaign }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination Controls -->
|
|
<div style="padding: 1.25rem 1.5rem; border-top: 1px solid var(--border-color); display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
|
|
<div style="font-size: 0.9rem; color: var(--text-secondary);">
|
|
Showing {{ page_start }}-{{ page_end }} of {{ total_records }} visits
|
|
</div>
|
|
|
|
<div class="pagination" style="display: flex; justify-content: center; align-items: center; gap: 0.5rem;">
|
|
{% if current_page > 1 %}
|
|
<a href="/admin/analytics/url/{{ url.id }}?analytics_page=1&date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
|
<a href="/admin/analytics/url/{{ url.id }}?analytics_page={{ current_page - 1 }}&date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</span>
|
|
{% endif %}
|
|
|
|
{% for p in visible_pages %}
|
|
{% if self.is_current(p) %}
|
|
<span class="btn btn-primary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem; font-weight: bold;">[{{ p }}]</span>
|
|
{% else %}
|
|
<a href="/admin/analytics/url/{{ url.id }}?analytics_page={{ p }}&date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">{{ p }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if current_page < total_pages %}
|
|
<a href="/admin/analytics/url/{{ url.id }}?analytics_page={{ current_page + 1 }}&date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Next ></a>
|
|
<a href="/admin/analytics/url/{{ url.id }}?analytics_page={{ total_pages }}&date_from={{ date_from.as_deref().unwrap_or("") }}&date_to={{ date_to.as_deref().unwrap_or("") }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Last >></a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">Next ></span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">Last >></span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|