📱 Mobile & Desktop Optimization: - Responsive grid layout: 1 card (mobile) → 2 cards (tablet) → 3 cards (desktop) → 4 cards (large screens) - Better card design with improved spacing and hover effects - Enhanced mobile experience with optimized touch targets 🔧 UI/UX Improvements: - Modern card-based layout instead of stacked list - Website logos with fallback icons for better visual recognition - Improved button placement and iconography - Better responsive breakpoints for different screen sizes ✨ Visual Enhancements: - Smooth hover animations and shadow effects - Better logo positioning with placeholder icons - Optimized spacing and typography for mobile devices - Professional card design with rounded corners and subtle shadows This update provides a much cleaner and more organized view of links, especially on mobile devices where the cards stack properly and buttons are easily accessible.
653 lines
20 KiB
HTML
653 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ page.title }} - Statistics</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 40px 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
font-weight: 300;
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1em;
|
|
opacity: 0.9;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stats {
|
|
background: rgba(255,255,255,0.1);
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 1.5em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9em;
|
|
opacity: 0.8;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.admin-info {
|
|
background: rgba(255,255,255,0.1);
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
text-align: left;
|
|
}
|
|
|
|
.admin-info h3 {
|
|
margin-bottom: 10px;
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.content {
|
|
padding: 30px;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.section h2 {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
font-size: 1.8em;
|
|
border-bottom: 2px solid #667eea;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.links-section h2 {
|
|
color: #333;
|
|
margin-bottom: 25px;
|
|
font-size: 1.8em;
|
|
text-align: center;
|
|
}
|
|
|
|
.link-item {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 15px;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 15px;
|
|
color: inherit;
|
|
}
|
|
|
|
.link-item:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.link-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.link-title {
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.link-logo {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.link-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.link-description {
|
|
color: #666;
|
|
font-size: 0.95em;
|
|
line-height: 1.4;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.link-url {
|
|
color: #667eea;
|
|
font-size: 0.9em;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #666;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: 1.5em;
|
|
margin-bottom: 15px;
|
|
color: #999;
|
|
}
|
|
|
|
.empty-state p {
|
|
font-size: 1.1em;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.actions {
|
|
background: #f8f9fa;
|
|
padding: 20px 30px;
|
|
border-top: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 15px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.qr-info {
|
|
background: #e8f4fd;
|
|
border: 1px solid #b8daff;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.qr-info h3 {
|
|
color: #0c5460;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.qr-info p {
|
|
color: #0c5460;
|
|
font-size: 0.9em;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.qr-url {
|
|
background: white;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
word-break: break-all;
|
|
border: 1px solid #b8daff;
|
|
}
|
|
|
|
.qr-preview-section {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-top: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.qr-preview-section h4 {
|
|
color: #495057;
|
|
margin-bottom: 15px;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.qr-preview-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.qr-preview-image {
|
|
max-width: 200px;
|
|
max-height: 200px;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 8px;
|
|
background: white;
|
|
padding: 10px;
|
|
}
|
|
|
|
.qr-download-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-download {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
font-size: 0.9em;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-download-png {
|
|
background: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.btn-download-svg {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.qr-loading {
|
|
color: #666;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Responsive design */
|
|
@media (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.container {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.header {
|
|
padding: 30px 20px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2em;
|
|
}
|
|
|
|
.content {
|
|
padding: 20px;
|
|
}
|
|
|
|
.actions {
|
|
padding: 15px 20px;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.stats {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.link-item {
|
|
padding: 15px;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.link-content {
|
|
width: 100%;
|
|
}
|
|
|
|
.link-title {
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.qr-preview-image {
|
|
max-width: 150px;
|
|
max-height: 150px;
|
|
}
|
|
|
|
.qr-download-actions {
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-download {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.header h1 {
|
|
font-size: 1.8em;
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1em;
|
|
}
|
|
|
|
.content {
|
|
padding: 15px;
|
|
}
|
|
|
|
.links-section h2 {
|
|
font-size: 1.5em;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>{{ page.title }} - Statistics</h1>
|
|
<p>{{ page.description }}</p>
|
|
|
|
<div class="stats">
|
|
<div class="stat-item">
|
|
<div class="stat-number">{{ page.links|length }}</div>
|
|
<div class="stat-label">Links</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-number">{{ page.view_count }}</div>
|
|
<div class="stat-label">Page Views</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-number">{{ page.links|sum(attribute='clicks') or 0 }}</div>
|
|
<div class="stat-label">Total Clicks</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-info">
|
|
<h3>📊 Page Information</h3>
|
|
<div class="info-row">
|
|
<span>Page ID:</span>
|
|
<span>{{ page.id }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span>Created:</span>
|
|
<span>{{ page.created_at or 'N/A' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span>Last Updated:</span>
|
|
<span>{{ page.updated_at or 'N/A' }}</span>
|
|
</div>
|
|
{% if page.short_url %}
|
|
<div class="info-row">
|
|
<span>Short URL:</span>
|
|
<span>{{ page.short_url }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<div class="section">
|
|
<div class="qr-info">
|
|
<h3>🔗 Public QR Code URL</h3>
|
|
<p>This is the URL that your QR code points to (public page without statistics):</p>
|
|
<div class="qr-url">{{ request.url_root }}links/{{ page.id }}</div>
|
|
|
|
<div class="qr-preview-section">
|
|
<h4>📱 QR Code Preview</h4>
|
|
<div class="qr-preview-container">
|
|
<div id="qr-preview-loading" class="qr-loading">Loading QR code...</div>
|
|
<img id="qr-preview-image" class="qr-preview-image" style="display: none;" alt="QR Code">
|
|
<div class="qr-download-actions" id="qr-download-actions" style="display: none;">
|
|
<button class="btn-download btn-download-png" onclick="downloadQRCode('png')">
|
|
📥 Download PNG
|
|
</button>
|
|
<button class="btn-download btn-download-svg" onclick="downloadQRCode('svg')">
|
|
🎨 Download SVG
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section links-section">
|
|
{% if page.links %}
|
|
<h2>📚 Links with Statistics</h2>
|
|
{% for link in page.links %}
|
|
<div class="link-item" style="cursor: default;">
|
|
<div class="link-content">
|
|
<div class="link-title">
|
|
{{ link.title }}
|
|
{% if link.short_url %}<span style="background: #2196f3; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 10px; margin-left: 8px;">SHORT</span>{% endif %}
|
|
</div>
|
|
{% if link.description %}
|
|
<div class="link-description">{{ link.description }}</div>
|
|
{% endif %}
|
|
<div class="link-url">{{ link.short_url if link.short_url else link.url }}</div>
|
|
<div style="margin-top: 10px; font-size: 0.85em; color: #666;">
|
|
<strong>Clicks:</strong> {{ link.clicks or 0 }} |
|
|
<strong>Added:</strong> {{ link.created_at or 'N/A' }}
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<img class="link-logo" style="display: none;" alt="Logo">
|
|
<div class="link-icon">📊</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<h3>No Links Yet</h3>
|
|
<p>This page doesn't have any links yet. Add some links to see statistics here.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<a href="/edit/{{ page.id }}" class="btn btn-primary">
|
|
✏️ Edit Page
|
|
</a>
|
|
<a href="/links/{{ page.id }}" class="btn btn-secondary" target="_blank">
|
|
👁️ View Public Page
|
|
</a>
|
|
<a href="/" class="btn btn-secondary">
|
|
🏠 Back to Dashboard
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Website logo mapping for better recognition
|
|
function getWebsiteLogo(url) {
|
|
try {
|
|
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
|
|
const domain = urlObj.hostname.replace('www.', '');
|
|
|
|
const logoMap = {
|
|
'youtube.com': 'https://www.youtube.com/favicon.ico',
|
|
'youtu.be': 'https://www.youtube.com/favicon.ico',
|
|
'facebook.com': 'https://www.facebook.com/favicon.ico',
|
|
'instagram.com': 'https://www.instagram.com/favicon.ico',
|
|
'twitter.com': 'https://abs.twimg.com/favicons/twitter.ico',
|
|
'x.com': 'https://abs.twimg.com/favicons/twitter.ico',
|
|
'linkedin.com': 'https://static.licdn.com/sc/h/al2o9zrvru7aqj8e1x2rzsrca',
|
|
'github.com': 'https://github.com/favicon.ico',
|
|
'stackoverflow.com': 'https://stackoverflow.com/favicon.ico',
|
|
'reddit.com': 'https://www.reddit.com/favicon.ico',
|
|
'medium.com': 'https://medium.com/favicon.ico',
|
|
'discord.com': 'https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.ico',
|
|
'twitch.tv': 'https://static.twitchcdn.net/assets/favicon-32-d6025c14e900565d6177.png',
|
|
'spotify.com': 'https://open.spotify.com/favicon.ico',
|
|
'apple.com': 'https://www.apple.com/favicon.ico',
|
|
'google.com': 'https://www.google.com/favicon.ico',
|
|
'microsoft.com': 'https://www.microsoft.com/favicon.ico',
|
|
'amazon.com': 'https://www.amazon.com/favicon.ico',
|
|
'netflix.com': 'https://assets.nflxext.com/ffe/siteui/common/icons/nficon2016.ico',
|
|
'whatsapp.com': 'https://static.whatsapp.net/rsrc.php/v3/yz/r/ujTY9i_Jhs1.png'
|
|
};
|
|
|
|
if (logoMap[domain]) {
|
|
return logoMap[domain];
|
|
}
|
|
|
|
// Fallback to favicon
|
|
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Set logos for links
|
|
function setLogosForLinks() {
|
|
document.querySelectorAll('[data-url]').forEach(linkElement => {
|
|
const url = linkElement.getAttribute('data-url');
|
|
const logoImg = linkElement.querySelector('.link-logo');
|
|
const fallbackIcon = linkElement.querySelector('.link-icon');
|
|
const logoSrc = getWebsiteLogo(url);
|
|
|
|
if (logoSrc && logoImg) {
|
|
logoImg.src = logoSrc;
|
|
logoImg.style.display = 'block';
|
|
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
|
|
// Hide the fallback icon when logo is shown
|
|
logoImg.onload = function() {
|
|
if (fallbackIcon) fallbackIcon.style.display = 'none';
|
|
};
|
|
logoImg.onerror = function() {
|
|
this.style.display = 'none';
|
|
if (fallbackIcon) fallbackIcon.style.display = 'flex';
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize logos when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setLogosForLinks();
|
|
loadQRCodePreview();
|
|
});
|
|
|
|
// QR Code functionality
|
|
let currentQRId = null;
|
|
|
|
async function loadQRCodePreview() {
|
|
try {
|
|
const pageId = '{{ page.id }}';
|
|
|
|
// Find the QR code for this page
|
|
const response = await fetch('/api/qr_codes');
|
|
const qrCodes = await response.json();
|
|
|
|
const pageQR = qrCodes.find(qr => qr.type === 'link_page' && qr.page_id === pageId);
|
|
|
|
if (pageQR) {
|
|
currentQRId = pageQR.id;
|
|
const previewImage = document.getElementById('qr-preview-image');
|
|
const loadingText = document.getElementById('qr-preview-loading');
|
|
const downloadActions = document.getElementById('qr-download-actions');
|
|
|
|
previewImage.src = pageQR.preview;
|
|
previewImage.style.display = 'block';
|
|
loadingText.style.display = 'none';
|
|
downloadActions.style.display = 'flex';
|
|
} else {
|
|
document.getElementById('qr-preview-loading').textContent = 'QR code not found';
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load QR code preview:', error);
|
|
document.getElementById('qr-preview-loading').textContent = 'Failed to load QR code';
|
|
}
|
|
}
|
|
|
|
function downloadQRCode(format) {
|
|
if (!currentQRId) {
|
|
alert('QR code not found');
|
|
return;
|
|
}
|
|
|
|
if (format === 'svg') {
|
|
window.open(`/api/download/${currentQRId}/svg`, '_blank');
|
|
} else {
|
|
window.open(`/api/download/${currentQRId}`, '_blank');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|