From e1bb95952effbaa6cb42be6016abab3f3cb4b8dc Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Tue, 23 Jun 2026 16:17:06 +0530 Subject: [PATCH 1/2] feat: add visual export functionality for dashboard analytics (#2712) --- src/app/dashboard/page.tsx | 15 +- src/components/ExportButton.tsx | 77 +++------ src/components/ExportModal.tsx | 283 ++++++++++++++++++++++++++++++++ 3 files changed, 309 insertions(+), 66 deletions(-) create mode 100644 src/components/ExportModal.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c33b4034c..894a009ad 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -129,7 +129,7 @@ export default async function DashboardPage() { return ( -
+
{/* Quick actions */} @@ -201,13 +201,12 @@ export default async function DashboardPage() { failedGoals: 1 }} /> - +
- {/* Repo analytics explorer — full width */} -
+
}> @@ -215,8 +214,8 @@ export default async function DashboardPage() { {/* -- Row 2: PR metrics + Community metrics -- */}
- - +
+
{/* PR breakdown + commit time — 2-col so charts have room */} @@ -230,14 +229,14 @@ export default async function DashboardPage() {
{/* Activity ring — full width */} -
+
}>
{/* Coding activity insights — full width */} -
+
}> diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index f4698a159..4c7cf17ed 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -4,6 +4,8 @@ import { useMemo, useState } from "react"; import { useSession } from "next-auth/react"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; +import ExportModal from "./ExportModal"; +import { Download } from "lucide-react"; interface PRData { open: number; @@ -270,6 +272,7 @@ export default function ExportButton() { const [isExportingCSV, setIsExportingCSV] = useState(false); const [isExportingPDF, setIsExportingPDF] = useState(false); const [isExportingJSON, setIsExportingJSON] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const reportName = useMemo( () => session?.githubLogin ?? session?.user?.name ?? "", @@ -701,68 +704,26 @@ export default function ExportButton() { }; return ( -
+ <> - - - -
+ setIsModalOpen(false)} + reportName={reportName} + onLegacyExport={async (type) => { + if (type === "csv") await exportCSV(); + if (type === "pdf") await exportPDF(); + if (type === "json") await exportJSON(); + }} + /> + ); } diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx new file mode 100644 index 000000000..158490b74 --- /dev/null +++ b/src/components/ExportModal.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useState } from "react"; +import * as htmlToImage from "html-to-image"; +import jsPDF from "jspdf"; +import { Download, LayoutDashboard, FileImage, FileText, Image as ImageIcon, X } from "lucide-react"; + +export interface ExportModalProps { + isOpen: boolean; + onClose: () => void; + onLegacyExport: (type: "csv" | "pdf" | "json") => Promise; + reportName: string; +} + +export default function ExportModal({ isOpen, onClose, onLegacyExport, reportName }: ExportModalProps) { + const [activeTab, setActiveTab] = useState<"visual" | "data">("visual"); + const [visualFormat, setVisualFormat] = useState<"png" | "jpeg" | "pdf">("png"); + const [selectedSection, setSelectedSection] = useState("dashboard-content"); + const [themePreference, setThemePreference] = useState<"current" | "light" | "dark">("current"); + const [isExporting, setIsExporting] = useState(false); + + if (!isOpen) return null; + + const sections = [ + { id: "dashboard-content", name: "Full Dashboard" }, + { id: "streak-tracker", name: "Streak Tracker" }, + { id: "repo-analytics", name: "Repo Analytics" }, + { id: "pr-metrics", name: "PR Metrics" }, + { id: "activity-ring", name: "Activity Ring" }, + { id: "coding-insights", name: "Coding Insights" }, + ]; + + const handleVisualExport = async () => { + setIsExporting(true); + try { + const element = document.getElementById(selectedSection); + if (!element) { + alert("Selected section not found on the page."); + return; + } + + // Small delay to ensure charts (like recharts) have finished animating + await new Promise((resolve) => setTimeout(resolve, 500)); + + const originalTheme = document.documentElement.className; + // eslint-disable-next-line react-hooks/immutability + if (themePreference === "light") document.documentElement.className = "light"; + // eslint-disable-next-line react-hooks/immutability + if (themePreference === "dark") document.documentElement.className = "dark"; + + // Re-trigger layout/paint for theme switch + if (themePreference !== "current") { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + const cloneConfig = { + quality: 1, + backgroundColor: themePreference === "light" ? "#ffffff" : themePreference === "dark" ? "#0f172a" : undefined, + style: { + transform: "scale(1)", + transformOrigin: "top left" + } + }; + + if (visualFormat === "pdf") { + const dataUrl = await htmlToImage.toPng(element, cloneConfig); + const pdf = new jsPDF("p", "mm", "a4"); + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = (element.offsetHeight * pdfWidth) / element.offsetWidth; + + pdf.addImage(dataUrl, "PNG", 0, 0, pdfWidth, pdfHeight); + pdf.save(`devtrack-visual-${reportName}-${new Date().toISOString().slice(0, 10)}.pdf`); + } else if (visualFormat === "png") { + const dataUrl = await htmlToImage.toPng(element, cloneConfig); + downloadUrl(dataUrl, `devtrack-${reportName}.${visualFormat}`); + } else { + const dataUrl = await htmlToImage.toJpeg(element, cloneConfig); + downloadUrl(dataUrl, `devtrack-${reportName}.${visualFormat}`); + } + + // Restore theme + if (themePreference !== "current") { + // eslint-disable-next-line react-hooks/immutability + document.documentElement.className = originalTheme; + } + } catch (error) { + console.error("Export failed", error); + alert("Failed to export visual report."); + } finally { + setIsExporting(false); + } + }; + + const downloadUrl = (url: string, filename: string) => { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; + + const handleLegacy = async (type: "csv" | "pdf" | "json") => { + setIsExporting(true); + try { + await onLegacyExport(type); + } finally { + setIsExporting(false); + } + }; + + return ( +
+
+ + {/* Header */} +
+

+ + Export Analytics +

+ +
+ + {/* Tabs */} +
+ + +
+ + {/* Content */} +
+ + {activeTab === "visual" ? ( +
+
+ +
+ {sections.map(s => ( + + ))} +
+
+ +
+ +
+ {["current", "light", "dark"].map(t => ( + + ))} +
+
+ +
+ +
+ {["png", "jpeg", "pdf"].map(f => ( + + ))} +
+
+ + +
+ ) : ( +
+
+ Download your analytics as raw data or text-based PDF reports for offline analysis or backup. +
+
+ + + +
+
+ )} +
+
+
+ ); +} From 044c7c3f5e66eb63fb1ed58bef14c2f0339bb0f1 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Thu, 25 Jun 2026 08:35:21 +0530 Subject: [PATCH 2/2] fix: resolve TS2353 for useResizeObserver in ResizeObserverConfig --- src/components/dashboard/SortableDashboardWidget.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/dashboard/SortableDashboardWidget.tsx b/src/components/dashboard/SortableDashboardWidget.tsx index a4c6eac6a..1f6ce40bd 100644 --- a/src/components/dashboard/SortableDashboardWidget.tsx +++ b/src/components/dashboard/SortableDashboardWidget.tsx @@ -34,9 +34,7 @@ export default function SortableDashboardWidget({ } = useSortable({ id, disabled: !isEditing, - resizeObserverConfig: { - useResizeObserver: true, - }, + resizeObserverConfig: {}, }); const style: CSSProperties = {