Add PDF export functionality and favicon support
- Introduced a new script for exporting the dashboard as a PDF using html2canvas and jsPDF. - Added a favicon.ico file and linked it in the base template. - Updated the base template to include the new PDF export script and modified the button for PDF export functionality. - Enhanced the index template to include an ID for the dashboard content for PDF generation.
This commit is contained in:
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
111
static/js/export-pdf.js
Normal file
111
static/js/export-pdf.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Export dashboard as PDF by capturing a screenshot of #dashboard-content
|
||||||
|
* and assembling it into a multi-page PDF (html2canvas + jsPDF).
|
||||||
|
*/
|
||||||
|
function exportDashboardToPdf() {
|
||||||
|
var el = document.getElementById('dashboard-content');
|
||||||
|
if (!el) {
|
||||||
|
if (typeof console !== 'undefined' && console.warn) {
|
||||||
|
console.warn('export-pdf: #dashboard-content not found');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = document.getElementById('pdf-export-btn');
|
||||||
|
var originalText = btn ? btn.innerHTML : '';
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = 'Generating PDF…';
|
||||||
|
}
|
||||||
|
|
||||||
|
var regionEl = document.getElementById('regionBadge');
|
||||||
|
var region = regionEl ? (regionEl.textContent || '').trim() : '';
|
||||||
|
|
||||||
|
if (typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
alert('PDF export requires html2canvas and jsPDF. Please refresh the page.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var JsPDFConstructor = (typeof jspdf !== 'undefined' && jspdf.jsPDF) ? jspdf.jsPDF : jspdf;
|
||||||
|
|
||||||
|
var auditSection = el.querySelector('section[aria-label="Audit analysis"]');
|
||||||
|
var auditSectionDisplay = '';
|
||||||
|
if (auditSection) {
|
||||||
|
auditSectionDisplay = auditSection.style.display;
|
||||||
|
auditSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreAuditSection() {
|
||||||
|
if (auditSection) {
|
||||||
|
auditSection.style.display = auditSectionDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html2canvas(el, {
|
||||||
|
scale: 2,
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false
|
||||||
|
}).then(function(canvas) {
|
||||||
|
var imgW = canvas.width;
|
||||||
|
var imgH = canvas.height;
|
||||||
|
var dataUrl = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
var doc = new JsPDFConstructor({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||||
|
var pageW = 210;
|
||||||
|
var pageH = 297;
|
||||||
|
var margin = 10;
|
||||||
|
var contentW = pageW - 2 * margin;
|
||||||
|
var headerH = 14;
|
||||||
|
var firstPageImgTop = margin + headerH;
|
||||||
|
var firstPageImgH = pageH - firstPageImgTop - margin;
|
||||||
|
var otherPageImgH = pageH - 2 * margin;
|
||||||
|
|
||||||
|
var imgWmm = contentW;
|
||||||
|
var imgHmm = contentW * (imgH / imgW);
|
||||||
|
|
||||||
|
doc.setFontSize(14);
|
||||||
|
doc.text('Dashboard report', margin, margin + 6);
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text(region ? 'Region: ' + region : '', margin, margin + 12);
|
||||||
|
|
||||||
|
var shown = 0;
|
||||||
|
var totalH = imgHmm;
|
||||||
|
var pageNum = 0;
|
||||||
|
var imgYmm = firstPageImgTop;
|
||||||
|
|
||||||
|
while (shown < totalH) {
|
||||||
|
if (pageNum > 0) {
|
||||||
|
doc.addPage();
|
||||||
|
imgYmm = margin;
|
||||||
|
}
|
||||||
|
var sliceH = pageNum === 0 ? firstPageImgH : otherPageImgH;
|
||||||
|
var yOffset = -shown;
|
||||||
|
doc.addImage(dataUrl, 'PNG', margin, imgYmm + yOffset, imgWmm, imgHmm);
|
||||||
|
shown += sliceH;
|
||||||
|
pageNum += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.save('dashboard-report.pdf');
|
||||||
|
|
||||||
|
restoreAuditSection();
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (typeof console !== 'undefined' && console.error) {
|
||||||
|
console.error('export-pdf:', err);
|
||||||
|
}
|
||||||
|
restoreAuditSection();
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
alert('Failed to generate PDF. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
10
static/js/html2canvas-pro.min.js
vendored
Normal file
10
static/js/html2canvas-pro.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
397
static/js/jspdf.umd.min.js
vendored
Normal file
397
static/js/jspdf.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,10 +5,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}SWatcher{% endblock %}</title>
|
<title>{% block title %}SWatcher{% endblock %}</title>
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
||||||
|
<script src="{% static 'js/html2canvas-pro.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/jspdf.umd.min.js' %}"></script>
|
||||||
{% block imports %}
|
{% block imports %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block css %}
|
{% block css %}
|
||||||
@@ -22,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="px-1 flex items-center gap-3 pr-10">
|
<div class="px-1 flex items-center gap-3 pr-10">
|
||||||
<button type="button" class="btn btn-ghost btn-sm no-print" onclick="window.print()" title="Save as PDF" aria-label="Save as PDF">
|
<button type="button" id="pdf-export-btn" class="btn btn-ghost btn-sm no-print" onclick="exportDashboardToPdf()" title="Save as PDF" aria-label="Save as PDF">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -70,6 +73,7 @@
|
|||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('theme', newTheme);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{% static 'js/export-pdf.js' %}"></script>
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- MAIN DASHBOARD -->
|
<!-- MAIN DASHBOARD -->
|
||||||
<div class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
|
<div id="dashboard-content" class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
|
||||||
<!-- QUICK STATS ROW -->
|
<!-- QUICK STATS ROW -->
|
||||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4" aria-label="Quick stats">
|
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4" aria-label="Quick stats">
|
||||||
<!-- CPU Utilization -->
|
<!-- CPU Utilization -->
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'favicon.ico', permanent=False)),
|
||||||
path('', include('dashboard.urls')),
|
path('', include('dashboard.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user