From 8349915d1ebe85f8a8a746bd5ad4b67da0945e9c Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 01:41:15 +0000 Subject: [PATCH] Add dashboard export functionality as PDF or image Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/7dd200c2-1934-480c-9df5-4d6187d23b0c.jpg --- .../components/dashboard/dashboard-export.tsx | 150 ++++++++++++++++++ client/src/pages/main-dashboard-page.tsx | 14 +- package-lock.json | 6 +- package.json | 1 + 4 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 client/src/components/dashboard/dashboard-export.tsx diff --git a/client/src/components/dashboard/dashboard-export.tsx b/client/src/components/dashboard/dashboard-export.tsx new file mode 100644 index 0000000..b4233e1 --- /dev/null +++ b/client/src/components/dashboard/dashboard-export.tsx @@ -0,0 +1,150 @@ +import React, { useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Download, Camera, FileText, Loader2 } from 'lucide-react'; +import html2canvas from 'html2canvas'; +import { jsPDF } from 'jspdf'; + +interface DashboardExportProps { + dashboardRef: React.RefObject; + title?: string; +} + +export function DashboardExport({ dashboardRef, title = 'Dashboard' }: DashboardExportProps) { + const [isExporting, setIsExporting] = useState(false); + + const handleExportPDF = async () => { + if (!dashboardRef.current) return; + + setIsExporting(true); + try { + const dashboard = dashboardRef.current; + + // Calculate height for proper scaling + const originalWidth = dashboard.offsetWidth; + const originalHeight = dashboard.offsetHeight; + + // A4 dimensions (in pixels at 96 DPI) + const pdfWidth = 595; + const pdfHeight = 842; + + // Calculate scale to fit width + const scale = pdfWidth / originalWidth; + const scaledHeight = originalHeight * scale; + + // Create canvas from DOM node + const canvas = await html2canvas(dashboard, { + scale: 2, // Higher quality + useCORS: true, + logging: false, + allowTaint: true, + backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff" + }); + + // Create PDF + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF({ + orientation: scaledHeight > pdfHeight ? 'landscape' : 'portrait', + unit: 'px', + format: 'a4' + }); + + // Add title + pdf.setFontSize(16); + pdf.text(title, 20, 20); + + // Calculate position to center image + const imgWidth = pdf.internal.pageSize.getWidth() - 40; // margins + const imgHeight = (canvas.height * imgWidth) / canvas.width; + + // Add image + pdf.addImage(imgData, 'PNG', 20, 30, imgWidth, imgHeight); + + // Save PDF + pdf.save(`${title.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.pdf`); + } catch (error) { + console.error('Error exporting PDF:', error); + } finally { + setIsExporting(false); + } + }; + + const handleExportImage = async () => { + if (!dashboardRef.current) return; + + setIsExporting(true); + try { + const dashboard = dashboardRef.current; + + // Create canvas from DOM node + const canvas = await html2canvas(dashboard, { + scale: 2, // Higher quality + useCORS: true, + logging: false, + allowTaint: true, + backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff" + }); + + // Convert to PNG + const imgData = canvas.toDataURL('image/png'); + + // Create a download link + const link = document.createElement('a'); + link.href = imgData; + link.download = `${title.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.png`; + link.click(); + } catch (error) { + console.error('Error exporting image:', error); + } finally { + setIsExporting(false); + } + }; + + return ( + + + + + +
+

Export Dashboard

+

Choose export format

+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/main-dashboard-page.tsx b/client/src/pages/main-dashboard-page.tsx index 45a145a..d4e6a9e 100644 --- a/client/src/pages/main-dashboard-page.tsx +++ b/client/src/pages/main-dashboard-page.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -29,6 +29,7 @@ import { Loader2 } from "lucide-react"; import { DashboardLayout } from "@/layouts/dashboard-layout"; +import { DashboardExport } from "@/components/dashboard/dashboard-export"; interface DashboardSummary { userCount: number; @@ -41,6 +42,9 @@ interface DashboardSummary { } export default function MainDashboardPage() { + // Create a ref for the dashboard content + const dashboardRef = useRef(null); + // Fetch summary data const { data: summary, isLoading: summaryLoading } = useQuery({ queryKey: ['/api/dashboard-summary'], @@ -101,13 +105,17 @@ export default function MainDashboardPage() { return ( +
+ +
+ {isLoading ? (
Loading dashboard data...
) : ( - <> +
{/* Summary Cards */}
@@ -270,7 +278,7 @@ export default function MainDashboardPage() {
- +
)}
); diff --git a/package-lock.json b/package-lock.json index 1389f1c..68d3f18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "express-session": "^1.18.1", "file-saver": "^2.0.5", "framer-motion": "^11.13.1", + "html2canvas": "^1.4.1", "input-otp": "^1.2.4", "ioredis": "^5.6.0", "jsonwebtoken": "^9.0.2", @@ -4093,7 +4094,6 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -4661,7 +4661,6 @@ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -6228,7 +6227,6 @@ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -8795,7 +8793,6 @@ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -9485,7 +9482,6 @@ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index ddad678..8078677 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "express-session": "^1.18.1", "file-saver": "^2.0.5", "framer-motion": "^11.13.1", + "html2canvas": "^1.4.1", "input-otp": "^1.2.4", "ioredis": "^5.6.0", "jsonwebtoken": "^9.0.2",