From d4b08abff36509694144976d969fc2c1098afe2c Mon Sep 17 00:00:00 2001 From: Srushti-Kamble Date: Wed, 20 May 2026 21:07:31 +0530 Subject: [PATCH 1/4] feat: add developer profile summary export --- src/components/ExportButton.tsx | 583 ++++++++++++++++++++++++-------- 1 file changed, 448 insertions(+), 135 deletions(-) diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index d7c6de9c..c4790ac7 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -1,8 +1,8 @@ + "use client"; import { useState } from "react"; import jsPDF from "jspdf"; -import autoTable from "jspdf-autotable"; interface PRData { open: number; @@ -11,11 +11,6 @@ interface PRData { mergeRate: string; } -interface DayData { - day: string; - commits: number; -} - interface Goal { id: string; label: string; @@ -23,8 +18,27 @@ interface Goal { current: number; } +interface ContributionResponse { + data: Record; +} + +interface StreakData { + current: number; + longest: number; + lastCommitDate?: string | null; + totalActiveDays?: number; +} +interface RepoData { + name?: string; + repo?: string; + commits?: number; + contributions?: number; + commitCount?: number; + description?: string; +} + export default function ExportButton() { - const [isExportingCSV, setIsExportingCSV] = useState(false); + const [isCopying, setIsCopying] = useState(false); const [isExportingPDF, setIsExportingPDF] = useState(false); const fetchData = async () => { @@ -32,173 +46,472 @@ export default function ExportButton() { cache: "no-store", }; - const [prRes, goalsRes, contribRes] = await Promise.all([ - fetch(`/api/metrics/prs`, fetchOptions), - fetch(`/api/goals`, fetchOptions), - fetch(`/api/metrics/contributions?days=365`, fetchOptions), - ]); + try { + const [ + prRes, + goalsRes, + contribRes, + streakRes, + reposRes, + ] = await Promise.all([ + fetch("/api/metrics/prs", fetchOptions), + fetch("/api/goals", fetchOptions), + fetch("/api/metrics/contributions?days=365", fetchOptions), + fetch("/api/metrics/streak", fetchOptions), + fetch("/api/metrics/repos", fetchOptions), + ]); - const prData: PRData | null = prRes.ok ? await prRes.json() : null; - const goalsData = goalsRes.ok ? await goalsRes.json() : { goals: [] }; - const contribDataRaw = contribRes.ok ? await contribRes.json() : { data: {} }; + + const prData: PRData | null = prRes.ok + ? await prRes.json() + : null; - const contribData: DayData[] = Object.entries(contribDataRaw.data ?? {}) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([day, commits]) => ({ day, commits: commits as number })); + + const goalsJson = goalsRes.ok + ? await goalsRes.json() + : { goals: [] }; - return { prData, contribData, goalsData: goalsData?.goals as Goal[] }; - }; + const goalsData: Goal[] = Array.isArray(goalsJson?.goals) + ? goalsJson.goals + : []; - const downloadFile = (content: string, filename: string, type: string) => { - const blob = new Blob([content], { type }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; + + const contribData: ContributionResponse = contribRes.ok + ? await contribRes.json() + : { data: {} }; - const exportCSV = async () => { - setIsExportingCSV(true); - try { - const { prData, goalsData, contribData } = await fetchData(); - - // FIX: Separate sheets using proper CSV sections without dashes - // PR Metrics sheet - let csv = "PR Metrics\n"; - csv += "Open,Merged,Avg Review Hours,Merge Rate\n"; - if (prData) { - csv += `${prData.open},${prData.merged},${prData.avgReviewHours},${prData.mergeRate}\n`; + + const streakData: StreakData | null = streakRes.ok + ? await streakRes.json() + : null; + + + const reposJson = reposRes.ok + ? await reposRes.json() + : { repos: [] }; + console.log("RAW REPOS RESPONSE:", reposJson); + + let reposData: RepoData[] = []; + + + if (Array.isArray(reposJson)) { + reposData = reposJson; + } else if (Array.isArray(reposJson?.repos)) { + reposData = reposJson.repos; + } else if (Array.isArray(reposJson?.data)) { + reposData = reposJson.data; } - // Contributions sheet - if (contribData && contribData.length > 0) { - csv += "\nCommit Activity\n"; - csv += "Date,Commits\n"; - contribData.forEach((d) => { - csv += `${d.day},${d.commits}\n`; - }); + return { + prData, + goalsData, + contribData, + streakData, + reposData, + }; + } catch (error) { + console.error("Fetch error:", error); + + return { + prData: null, + goalsData: [], + contribData: { data: {} }, + streakData: null, + reposData: [], + }; + } + }; + + +const buildSummary = async () => { + const { + prData, + goalsData, + contribData, + streakData, + reposData, + } = await fetchData(); + + + const contributionEntries = Object.entries( + contribData?.data || {} + ); + + const totalCommits = contributionEntries.reduce( + (acc, [, value]) => acc + Number(value || 0), + 0 + ); + + + const completedGoals = goalsData.filter( + (goal) => + Number(goal.current) >= Number(goal.target) + ).length; + + + let bestDayCount = 0; + let bestDayLabel = "—"; + + for (const [date, count] of contributionEntries) { + if (Number(count) > bestDayCount) { + bestDayCount = Number(count); + + bestDayLabel = new Date(date).toLocaleDateString( + "en-US", + { + month: "short", + day: "numeric", + year: "numeric", + } + ); + } + } + + const weeklyData: Record = {}; + + contributionEntries.forEach(([date, count]) => { + const d = new Date(date); + + const firstDay = new Date(d); + + const day = d.getDay(); + + const diff = + firstDay.getDate() - + day + + (day === 0 ? -6 : 1); + + firstDay.setDate(diff); + + const weekKey = firstDay + .toISOString() + .slice(0, 10); + + weeklyData[weekKey] = + (weeklyData[weekKey] || 0) + + Number(count); + }); + + let bestWeekCount = 0; + let bestWeekLabel = "—"; + + Object.entries(weeklyData).forEach( + ([week, count]) => { + if (count > bestWeekCount) { + bestWeekCount = count; + + bestWeekLabel = `Week of ${new Date( + week + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })}`; } + } + ); + + const monthlyData: Record = {}; + + contributionEntries.forEach(([date, count]) => { + const monthKey = date.slice(0, 7); + + monthlyData[monthKey] = + (monthlyData[monthKey] || 0) + + Number(count); + }); + + let bestMonthCount = 0; + let bestMonthLabel = "—"; + + Object.entries(monthlyData).forEach( + ([month, count]) => { + if (count > bestMonthCount) { + bestMonthCount = count; - // Goals sheet - if (goalsData && goalsData.length > 0) { - csv += "\nGoals\n"; - csv += "Label,Current,Target,Progress (%)\n"; - goalsData.forEach((g) => { - const pct = g.target > 0 ? ((g.current / g.target) * 100).toFixed(1) : "0"; - csv += `"${g.label}",${g.current},${g.target},${pct}%\n`; + const [year, mon] = month.split("-"); + + bestMonthLabel = new Date( + Number(year), + Number(mon) - 1 + ).toLocaleDateString("en-US", { + month: "long", + year: "numeric", }); } + } + ); + + + const sortedRepos = [...reposData].sort( + (a: any, b: any) => + Number( + b.commits || + b.contributions || + b.commitCount || + b.totalCommits || + b.contributionCount || + 0 + ) - + Number( + a.commits || + a.contributions || + a.commitCount || + a.totalCommits || + a.contributionCount || + 0 + ) + ); + + const topRepo = sortedRepos[0]; - downloadFile(csv, "dashboard-metrics.csv", "text/csv"); + const repoCommits = + Number( + topRepo?.commits || + topRepo?.contributions || + topRepo?.commitCount + ) || 0; + + const currentStreak = + Number(streakData?.current) || 0; + +const longestStreak = + Number(streakData?.longest) || 0; + + + const summary = ` +🚀 DevTrack Developer Productivity Summary + +Hey everyone !! + +Excited to share my latest developer productivity snapshot powered by DevTrack! + +🔥 Current Streak: ${currentStreak} days + +🏆 Longest Streak: ${longestStreak} days + +📦 Total Contributions: ${totalCommits} commits + +⚡ Best Day: ${bestDayCount} commits (${bestDayLabel}) + +🔥 Best Week: ${bestWeekCount} commits (${bestWeekLabel}) + +📅 Most Active Month: ${bestMonthCount} commits (${bestMonthLabel}) + +🔀 PR Merge Rate: ${ + prData?.mergeRate || "0%" + } + +⭐ Top Repository: ${ + topRepo?.name || + topRepo?.repo || + "N/A" + } + +Consistent progress is better than perfect progress. + +Looking forward to building more, contributing more, and learning every single day 🚀 + +#DevTrack #OpenSource #GitHub #WebDevelopment #GSSoC #DeveloperJourney +`; + + return summary; +}; + const copySummary = async () => { + setIsCopying(true); + + try { + const summary = await buildSummary(); + + await navigator.clipboard.writeText(summary); + + alert("Profile summary copied to clipboard!"); + } catch (error) { + console.error(error); + alert("Failed to copy summary."); } finally { - setIsExportingCSV(false); + setIsCopying(false); } }; const exportPDF = async () => { - setIsExportingPDF(true); - try { - const { prData, goalsData, contribData } = await fetchData(); - const doc = new jsPDF(); - - // Title - doc.setFontSize(20); - doc.setTextColor(40, 40, 40); - doc.text("Dashboard Metrics Export", 14, 20); - doc.setFontSize(10); - doc.setTextColor(120, 120, 120); - doc.text(`Generated on ${new Date().toLocaleDateString()}`, 14, 27); - - // FIX: Track Y position properly after each table - let currentY = 35; - - // PR Analytics section - if (prData) { - doc.setFontSize(13); - doc.setTextColor(40, 40, 40); - doc.text("PR Analytics", 14, currentY); - autoTable(doc, { - startY: currentY + 5, - head: [["Open PRs", "Merged", "Avg Review Time", "Merge Rate"]], - body: [[ - prData.open, - prData.merged, - `${prData.avgReviewHours}h`, - prData.mergeRate, - ]], - styles: { fontSize: 10 }, - headStyles: { fillColor: [59, 130, 246] }, - }); - // FIX: Update currentY after table using lastAutoTable - currentY = (doc as any).lastAutoTable.finalY + 12; - } + setIsExportingPDF(true); - // Goals section - if (goalsData && goalsData.length > 0) { - doc.setFontSize(13); - doc.setTextColor(40, 40, 40); - doc.text("Goals Tracker", 14, currentY); - autoTable(doc, { - startY: currentY + 5, - head: [["Goal Label", "Current", "Target", "Progress"]], - body: goalsData.map((g) => { - const pct = g.target > 0 ? ((g.current / g.target) * 100).toFixed(1) : "0"; - return [g.label, g.current, g.target, `${pct}%`]; - }), - styles: { fontSize: 10 }, - headStyles: { fillColor: [59, 130, 246] }, - }); - currentY = (doc as any).lastAutoTable.finalY + 12; + try { + + const summary = await buildSummary(); + + const cleanSummary = summary + .replace(/🚀/g, "") + .replace(/🔥/g, "") + .replace(/📦/g, "") + .replace(/🔀/g, "") + .replace(/⭐/g, "") + .replace(/📅/g, "") + .replace(/⚡/g, "") + .replace(/🏆/g, "") + .replace(/📝/g, "") + .replace(/📖/g, "") + .replace(/🎯/g, ""); + + const doc = new jsPDF({ + orientation: "portrait", + unit: "mm", + format: "a4", + }); + + const pageWidth = + doc.internal.pageSize.getWidth(); + + const pageHeight = + doc.internal.pageSize.getHeight(); + + // HEADER + doc.setFillColor(15, 23, 42); + + doc.rect(0, 0, pageWidth, 28, "F"); + + doc.setTextColor(255, 255, 255); + + doc.setFont("helvetica", "bold"); + + doc.setFontSize(18); + + doc.text( + "DevTrack Productivity Summary", + 14, + 18 + ); + + doc.setFontSize(10); + + doc.text( + `Generated on ${new Date().toLocaleDateString()}`, + 14, + 24 + ); + + + doc.setTextColor(40, 40, 40); + + doc.setFont("helvetica", "normal"); + + doc.setFontSize(11); + + const lines = doc.splitTextToSize( + cleanSummary, + 180 + ); + + let y = 40; + + lines.forEach((line: string) => { + + if (y > pageHeight - 20) { + doc.addPage(); + y = 20; } - // Commit Activity section - if (contribData && contribData.length > 0) { - doc.setFontSize(13); - doc.setTextColor(40, 40, 40); - doc.text("Commit Activity", 14, currentY); - autoTable(doc, { - startY: currentY + 5, - head: [["Date", "Commits"]], - body: contribData.map((d) => [d.day, d.commits]), - styles: { fontSize: 10 }, - headStyles: { fillColor: [59, 130, 246] }, - }); + if ( + line.includes("Current Streak") || + line.includes("Longest Streak") || + line.includes("Total Contributions") || + line.includes("Best Day") || + line.includes("Best Week") || + line.includes("Most Active Month") || + line.includes("PR Merge Rate") || + line.includes("Top Repository") || + line.includes("Repository Activity") || + line.includes("Goals Completed") + ) { + doc.setFont("helvetica", "bold"); + } else { + doc.setFont("helvetica", "normal"); } - doc.save("dashboard-metrics.pdf"); - } finally { - setIsExportingPDF(false); + doc.text(line, 14, y); + + y += 7; + }); + + + const totalPages = doc.getNumberOfPages(); + + for (let i = 1; i <= totalPages; i++) { + doc.setPage(i); + + doc.setFontSize(9); + + doc.setTextColor(120); + + doc.text( + `Page ${i} of ${totalPages}`, + pageWidth - 30, + pageHeight - 10 + ); } - }; + doc.save("devtrack-profile-summary.pdf"); + } catch (error) { + console.error(error); + + alert("Failed to export PDF."); + } finally { + setIsExportingPDF(false); + } +}; return ( -
+
+ +
); From 421fce2cfc9a550b7ed45bd989e407c114c3558d Mon Sep 17 00:00:00 2001 From: Srushti-Kamble Date: Thu, 21 May 2026 00:38:07 +0530 Subject: [PATCH 2/4] fix: address PR review comments --- src/components/ExportButton.tsx | 177 ++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 57 deletions(-) diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index 4d621002..02edc80f 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -40,6 +40,8 @@ interface RepoData { export default function ExportButton() { const [isCopying, setIsCopying] = useState(false); const [isExportingPDF, setIsExportingPDF] = useState(false); + const [isExportingCSV, setIsExportingCSV] = useState(false); + const [copied, setCopied] = useState(false); const fetchData = async () => { const fetchOptions: RequestInit = { @@ -89,7 +91,7 @@ export default function ExportButton() { const reposJson = reposRes.ok ? await reposRes.json() : { repos: [] }; - console.log("RAW REPOS RESPONSE:", reposJson); + let reposData: RepoData[] = []; @@ -244,33 +246,22 @@ const buildSummary = async () => { const sortedRepos = [...reposData].sort( - (a: any, b: any) => + (a, b) => Number( b.commits || b.contributions || - b.commitCount || - b.totalCommits || - b.contributionCount || - 0 + b.commitCount ) - Number( a.commits || a.contributions || - a.commitCount || - a.totalCommits || - a.contributionCount || - 0 + a.commitCount ) ); const topRepo = sortedRepos[0]; - const repoCommits = - Number( - topRepo?.commits || - topRepo?.contributions || - topRepo?.commitCount - ) || 0; + const currentStreak = Number(streakData?.current) || 0; @@ -308,6 +299,8 @@ Excited to share my latest developer productivity snapshot powered by DevTrack! "N/A" } + 🎯 Goals Completed: ${completedGoals}/${goalsData.length} + Consistent progress is better than perfect progress. Looking forward to building more, contributing more, and learning every single day 🚀 @@ -317,23 +310,68 @@ Looking forward to building more, contributing more, and learning every single d return summary; }; - const copySummary = async () => { - setIsCopying(true); - try { - const summary = await buildSummary(); +const downloadFile = ( + content: string, + filename: string, + type: string +) => { + const blob = new Blob([content], { type }); - await navigator.clipboard.writeText(summary); + const url = URL.createObjectURL(blob); - alert("Profile summary copied to clipboard!"); - } catch (error) { - console.error(error); - alert("Failed to copy summary."); - } finally { - setIsCopying(false); - } - }; + const a = document.createElement("a"); + + a.href = url; + + a.download = filename; + + document.body.appendChild(a); + + a.click(); + + document.body.removeChild(a); + + URL.revokeObjectURL(url); +}; + +const exportCSV = async () => { + setIsExportingCSV(true); + + try { + const summary = await buildSummary(); + + downloadFile( + summary, + "devtrack-summary.csv", + "text/csv" + ); + } catch (error) { + console.error(error); + } finally { + setIsExportingCSV(false); + } +}; + + const copySummary = async () => { + setIsCopying(true); + + try { + const summary = await buildSummary(); + await navigator.clipboard.writeText(summary); + + setCopied(true); + + setTimeout(() => { + setCopied(false); + }, 2000); + } catch (error) { + console.error("Failed to copy summary.", error); + } finally { + setIsCopying(false); + } +}; const exportPDF = async () => { setIsExportingPDF(true); @@ -366,7 +404,7 @@ Looking forward to building more, contributing more, and learning every single d const pageHeight = doc.internal.pageSize.getHeight(); - // HEADER + doc.setFillColor(15, 23, 42); doc.rect(0, 0, pageWidth, 28, "F"); @@ -453,9 +491,8 @@ Looking forward to building more, contributing more, and learning every single d doc.save("devtrack-profile-summary.pdf"); } catch (error) { - console.error(error); - - alert("Failed to export PDF."); + + console.error("Failed to export PDF.", error); } finally { setIsExportingPDF(false); } @@ -464,30 +501,56 @@ Looking forward to building more, contributing more, and learning every single d
+ type="button" + onClick={exportCSV} + disabled={isExportingCSV} + className="px-4 py-2 bg-[var(--control)] border border-[var(--border)] text-[var(--card-foreground)] hover:border-[var(--accent)] rounded-lg text-sm font-medium transition-colors flex items-center gap-2 disabled:opacity-50" +> + + + + + {isExportingCSV + ? "Exporting..." + : "Export CSV"} + +
); -} \ No newline at end of file +} From b65a6c977bee9252c906999fd88567c6cc376e12 Mon Sep 17 00:00:00 2001 From: Srushti-Kamble Date: Thu, 21 May 2026 14:00:25 +0530 Subject: [PATCH 3/4] feat: enhance CSV export functionality with detailed metrics --- src/components/ExportButton.tsx | 61 +++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index 02edc80f..e8ff14fe 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -339,20 +339,67 @@ const exportCSV = async () => { setIsExportingCSV(true); try { - const summary = await buildSummary(); + const { + prData, + goalsData, + contribData, + streakData, + } = await fetchData(); + + const contributionEntries = Object.entries( + contribData?.data || {} + ); + + const totalCommits = contributionEntries.reduce( + (acc, [, value]) => acc + Number(value || 0), + 0 + ); + + const completedGoals = goalsData.filter( + (goal) => + Number(goal.current) >= Number(goal.target) + ).length; + + const csvRows = [ + "PR Metrics", + "Open,Merged,Avg Review Hours,Merge Rate", + `${prData?.open || 0},${prData?.merged || 0},${prData?.avgReviewHours || 0},${prData?.mergeRate || "0%"}`, + "", + + "Contribution Metrics", + "Total Contributions,Current Streak,Longest Streak", + `${totalCommits},${streakData?.current || 0},${streakData?.longest || 0}`, + "", + + "Goals", +"Goal,Current,Target,Completed", + +...(goalsData.length > 0 + ? goalsData.map( + (goal) => + `"${goal.label}",${goal.current},${goal.target},${ + Number(goal.current) >= + Number(goal.target) + ? "Yes" + : "No" + }` + ) + : ["NA,NA,NA,NA"]), + ]; + + const csvContent = csvRows.join("\n"); downloadFile( - summary, - "devtrack-summary.csv", - "text/csv" + csvContent, + "devtrack-dashboard-metrics.csv", + "text/csv;charset=utf-8;" ); } catch (error) { - console.error(error); + console.error("Failed to export CSV.", error); } finally { setIsExportingCSV(false); } }; - const copySummary = async () => { setIsCopying(true); @@ -579,3 +626,5 @@ const exportCSV = async () => {
); } + + From b9662b4213ab53becde65544878e5ab869417828 Mon Sep 17 00:00:00 2001 From: Srushti-Kamble14 Date: Thu, 21 May 2026 21:07:25 +0530 Subject: [PATCH 4/4] Refactor ExportButton component and simplify data fetching --- src/components/ExportButton.tsx | 711 +++++++------------------------- 1 file changed, 143 insertions(+), 568 deletions(-) diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index e8ff14fe..b742548b 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -1,8 +1,8 @@ - "use client"; import { useState } from "react"; import jsPDF from "jspdf"; +import autoTable from "jspdf-autotable"; interface PRData { open: number; @@ -11,6 +11,11 @@ interface PRData { mergeRate: string; } +interface DayData { + day: string; + commits: number; +} + interface Goal { id: string; label: string; @@ -18,613 +23,183 @@ interface Goal { current: number; } -interface ContributionResponse { - data: Record; -} - -interface StreakData { - current: number; - longest: number; - lastCommitDate?: string | null; - totalActiveDays?: number; -} -interface RepoData { - name?: string; - repo?: string; - commits?: number; - contributions?: number; - commitCount?: number; - description?: string; -} - export default function ExportButton() { - const [isCopying, setIsCopying] = useState(false); - const [isExportingPDF, setIsExportingPDF] = useState(false); const [isExportingCSV, setIsExportingCSV] = useState(false); - const [copied, setCopied] = useState(false); + const [isExportingPDF, setIsExportingPDF] = useState(false); const fetchData = async () => { const fetchOptions: RequestInit = { cache: "no-store", }; - try { - const [ - prRes, - goalsRes, - contribRes, - streakRes, - reposRes, - ] = await Promise.all([ - fetch("/api/metrics/prs", fetchOptions), - fetch("/api/goals", fetchOptions), - fetch("/api/metrics/contributions?days=365", fetchOptions), - fetch("/api/metrics/streak", fetchOptions), - fetch("/api/metrics/repos", fetchOptions), - ]); - - - const prData: PRData | null = prRes.ok - ? await prRes.json() - : null; - - - const goalsJson = goalsRes.ok - ? await goalsRes.json() - : { goals: [] }; + const [prRes, goalsRes, contribRes] = await Promise.all([ + fetch(`/api/metrics/prs`, fetchOptions), + fetch(`/api/goals`, fetchOptions), + fetch(`/api/metrics/contributions?days=365`, fetchOptions), + ]); - const goalsData: Goal[] = Array.isArray(goalsJson?.goals) - ? goalsJson.goals - : []; + const prData: PRData | null = prRes.ok ? await prRes.json() : null; + const goalsData = goalsRes.ok ? await goalsRes.json() : { goals: [] }; + const contribDataRaw = contribRes.ok ? await contribRes.json() : { data: {} }; - - const contribData: ContributionResponse = contribRes.ok - ? await contribRes.json() - : { data: {} }; + const contribData: DayData[] = Object.entries(contribDataRaw.data ?? {}) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([day, commits]) => ({ day, commits: commits as number })); - - const streakData: StreakData | null = streakRes.ok - ? await streakRes.json() - : null; - - - const reposJson = reposRes.ok - ? await reposRes.json() - : { repos: [] }; - - - let reposData: RepoData[] = []; - - - if (Array.isArray(reposJson)) { - reposData = reposJson; - } else if (Array.isArray(reposJson?.repos)) { - reposData = reposJson.repos; - } else if (Array.isArray(reposJson?.data)) { - reposData = reposJson.data; - } - - return { - prData, - goalsData, - contribData, - streakData, - reposData, - }; - } catch (error) { - console.error("Fetch error:", error); - - return { - prData: null, - goalsData: [], - contribData: { data: {} }, - streakData: null, - reposData: [], - }; - } + return { prData, contribData, goalsData: goalsData?.goals as Goal[] }; }; + const downloadFile = (content: string, filename: string, type: string) => { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; -const buildSummary = async () => { - const { - prData, - goalsData, - contribData, - streakData, - reposData, - } = await fetchData(); - - - const contributionEntries = Object.entries( - contribData?.data || {} - ); - - const totalCommits = contributionEntries.reduce( - (acc, [, value]) => acc + Number(value || 0), - 0 - ); - - - const completedGoals = goalsData.filter( - (goal) => - Number(goal.current) >= Number(goal.target) - ).length; - - - let bestDayCount = 0; - let bestDayLabel = "—"; - - for (const [date, count] of contributionEntries) { - if (Number(count) > bestDayCount) { - bestDayCount = Number(count); - - bestDayLabel = new Date(date).toLocaleDateString( - "en-US", - { - month: "short", - day: "numeric", - year: "numeric", - } - ); - } - } - - const weeklyData: Record = {}; - - contributionEntries.forEach(([date, count]) => { - const d = new Date(date); - - const firstDay = new Date(d); - - const day = d.getDay(); - - const diff = - firstDay.getDate() - - day + - (day === 0 ? -6 : 1); - - firstDay.setDate(diff); - - const weekKey = firstDay - .toISOString() - .slice(0, 10); - - weeklyData[weekKey] = - (weeklyData[weekKey] || 0) + - Number(count); - }); - - let bestWeekCount = 0; - let bestWeekLabel = "—"; - - Object.entries(weeklyData).forEach( - ([week, count]) => { - if (count > bestWeekCount) { - bestWeekCount = count; - - bestWeekLabel = `Week of ${new Date( - week - ).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })}`; + const exportCSV = async () => { + setIsExportingCSV(true); + try { + const { prData, goalsData, contribData } = await fetchData(); + + // FIX: Separate sheets using proper CSV sections without dashes + // PR Metrics sheet + let csv = "PR Metrics\n"; + csv += "Open,Merged,Avg Review Hours,Merge Rate\n"; + if (prData) { + csv += `${prData.open},${prData.merged},${prData.avgReviewHours},${prData.mergeRate}\n`; } - } - ); - - const monthlyData: Record = {}; - - contributionEntries.forEach(([date, count]) => { - const monthKey = date.slice(0, 7); - - monthlyData[monthKey] = - (monthlyData[monthKey] || 0) + - Number(count); - }); - - let bestMonthCount = 0; - let bestMonthLabel = "—"; - Object.entries(monthlyData).forEach( - ([month, count]) => { - if (count > bestMonthCount) { - bestMonthCount = count; - - const [year, mon] = month.split("-"); - - bestMonthLabel = new Date( - Number(year), - Number(mon) - 1 - ).toLocaleDateString("en-US", { - month: "long", - year: "numeric", + // Contributions sheet + if (contribData && contribData.length > 0) { + csv += "\nCommit Activity\n"; + csv += "Date,Commits\n"; + contribData.forEach((d) => { + csv += `${d.day},${d.commits}\n`; }); } - } - ); - - - const sortedRepos = [...reposData].sort( - (a, b) => - Number( - b.commits || - b.contributions || - b.commitCount - ) - - Number( - a.commits || - a.contributions || - a.commitCount - ) - ); - - const topRepo = sortedRepos[0]; - - - - const currentStreak = - Number(streakData?.current) || 0; - -const longestStreak = - Number(streakData?.longest) || 0; - - - const summary = ` -🚀 DevTrack Developer Productivity Summary - -Hey everyone !! - -Excited to share my latest developer productivity snapshot powered by DevTrack! - -🔥 Current Streak: ${currentStreak} days - -🏆 Longest Streak: ${longestStreak} days - -📦 Total Contributions: ${totalCommits} commits - -⚡ Best Day: ${bestDayCount} commits (${bestDayLabel}) - -🔥 Best Week: ${bestWeekCount} commits (${bestWeekLabel}) - -📅 Most Active Month: ${bestMonthCount} commits (${bestMonthLabel}) - -🔀 PR Merge Rate: ${ - prData?.mergeRate || "0%" - } - -⭐ Top Repository: ${ - topRepo?.name || - topRepo?.repo || - "N/A" - } - 🎯 Goals Completed: ${completedGoals}/${goalsData.length} - -Consistent progress is better than perfect progress. - -Looking forward to building more, contributing more, and learning every single day 🚀 - -#DevTrack #OpenSource #GitHub #WebDevelopment #GSSoC #DeveloperJourney -`; - - return summary; -}; - -const downloadFile = ( - content: string, - filename: string, - type: string -) => { - const blob = new Blob([content], { type }); - - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - - a.href = url; - - a.download = filename; - - document.body.appendChild(a); - - a.click(); - - document.body.removeChild(a); - - URL.revokeObjectURL(url); -}; - -const exportCSV = async () => { - setIsExportingCSV(true); - - try { - const { - prData, - goalsData, - contribData, - streakData, - } = await fetchData(); - - const contributionEntries = Object.entries( - contribData?.data || {} - ); - - const totalCommits = contributionEntries.reduce( - (acc, [, value]) => acc + Number(value || 0), - 0 - ); - - const completedGoals = goalsData.filter( - (goal) => - Number(goal.current) >= Number(goal.target) - ).length; - - const csvRows = [ - "PR Metrics", - "Open,Merged,Avg Review Hours,Merge Rate", - `${prData?.open || 0},${prData?.merged || 0},${prData?.avgReviewHours || 0},${prData?.mergeRate || "0%"}`, - "", - - "Contribution Metrics", - "Total Contributions,Current Streak,Longest Streak", - `${totalCommits},${streakData?.current || 0},${streakData?.longest || 0}`, - "", - - "Goals", -"Goal,Current,Target,Completed", - -...(goalsData.length > 0 - ? goalsData.map( - (goal) => - `"${goal.label}",${goal.current},${goal.target},${ - Number(goal.current) >= - Number(goal.target) - ? "Yes" - : "No" - }` - ) - : ["NA,NA,NA,NA"]), - ]; - - const csvContent = csvRows.join("\n"); - - downloadFile( - csvContent, - "devtrack-dashboard-metrics.csv", - "text/csv;charset=utf-8;" - ); - } catch (error) { - console.error("Failed to export CSV.", error); - } finally { - setIsExportingCSV(false); - } -}; - const copySummary = async () => { - setIsCopying(true); - - try { - const summary = await buildSummary(); - - await navigator.clipboard.writeText(summary); + // Goals sheet + if (goalsData && goalsData.length > 0) { + csv += "\nGoals\n"; + csv += "Label,Current,Target,Progress (%)\n"; + goalsData.forEach((g) => { + const pct = g.target > 0 ? ((g.current / g.target) * 100).toFixed(1) : "0"; + csv += `"${g.label}",${g.current},${g.target},${pct}%\n`; + }); + } - setCopied(true); + downloadFile(csv, "dashboard-metrics.csv", "text/csv"); + } finally { + setIsExportingCSV(false); + } + }; - setTimeout(() => { - setCopied(false); - }, 2000); - } catch (error) { - console.error("Failed to copy summary.", error); - } finally { - setIsCopying(false); - } -}; const exportPDF = async () => { - setIsExportingPDF(true); - - try { - - const summary = await buildSummary(); - - const cleanSummary = summary - .replace(/🚀/g, "") - .replace(/🔥/g, "") - .replace(/📦/g, "") - .replace(/🔀/g, "") - .replace(/⭐/g, "") - .replace(/📅/g, "") - .replace(/⚡/g, "") - .replace(/🏆/g, "") - .replace(/📝/g, "") - .replace(/📖/g, "") - .replace(/🎯/g, ""); - - const doc = new jsPDF({ - orientation: "portrait", - unit: "mm", - format: "a4", - }); - - const pageWidth = - doc.internal.pageSize.getWidth(); - - const pageHeight = - doc.internal.pageSize.getHeight(); - - - doc.setFillColor(15, 23, 42); - - doc.rect(0, 0, pageWidth, 28, "F"); - - doc.setTextColor(255, 255, 255); - - doc.setFont("helvetica", "bold"); - - doc.setFontSize(18); - - doc.text( - "DevTrack Productivity Summary", - 14, - 18 - ); - - doc.setFontSize(10); - - doc.text( - `Generated on ${new Date().toLocaleDateString()}`, - 14, - 24 - ); - - - doc.setTextColor(40, 40, 40); - - doc.setFont("helvetica", "normal"); - - doc.setFontSize(11); - - const lines = doc.splitTextToSize( - cleanSummary, - 180 - ); - - let y = 40; - - lines.forEach((line: string) => { - - if (y > pageHeight - 20) { - doc.addPage(); - y = 20; + setIsExportingPDF(true); + try { + const { prData, goalsData, contribData } = await fetchData(); + const doc = new jsPDF(); + + // Title + doc.setFontSize(20); + doc.setTextColor(40, 40, 40); + doc.text("Dashboard Metrics Export", 14, 20); + doc.setFontSize(10); + doc.setTextColor(120, 120, 120); + doc.text(`Generated on ${new Date().toLocaleDateString()}`, 14, 27); + + // FIX: Track Y position properly after each table + let currentY = 35; + + // PR Analytics section + if (prData) { + doc.setFontSize(13); + doc.setTextColor(40, 40, 40); + doc.text("PR Analytics", 14, currentY); + autoTable(doc, { + startY: currentY + 5, + head: [["Open PRs", "Merged", "Avg Review Time", "Merge Rate"]], + body: [[ + prData.open, + prData.merged, + `${prData.avgReviewHours}h`, + prData.mergeRate, + ]], + styles: { fontSize: 10 }, + headStyles: { fillColor: [59, 130, 246] }, + }); + // FIX: Update currentY after table using lastAutoTable + currentY = (doc as any).lastAutoTable.finalY + 12; } - if ( - line.includes("Current Streak") || - line.includes("Longest Streak") || - line.includes("Total Contributions") || - line.includes("Best Day") || - line.includes("Best Week") || - line.includes("Most Active Month") || - line.includes("PR Merge Rate") || - line.includes("Top Repository") || - line.includes("Repository Activity") || - line.includes("Goals Completed") - ) { - doc.setFont("helvetica", "bold"); - } else { - doc.setFont("helvetica", "normal"); + // Goals section + if (goalsData && goalsData.length > 0) { + doc.setFontSize(13); + doc.setTextColor(40, 40, 40); + doc.text("Goals Tracker", 14, currentY); + autoTable(doc, { + startY: currentY + 5, + head: [["Goal Label", "Current", "Target", "Progress"]], + body: goalsData.map((g) => { + const pct = g.target > 0 ? ((g.current / g.target) * 100).toFixed(1) : "0"; + return [g.label, g.current, g.target, `${pct}%`]; + }), + styles: { fontSize: 10 }, + headStyles: { fillColor: [59, 130, 246] }, + }); + currentY = (doc as any).lastAutoTable.finalY + 12; } - doc.text(line, 14, y); - - y += 7; - }); - - - const totalPages = doc.getNumberOfPages(); - - for (let i = 1; i <= totalPages; i++) { - doc.setPage(i); - - doc.setFontSize(9); - - doc.setTextColor(120); + // Commit Activity section + if (contribData && contribData.length > 0) { + doc.setFontSize(13); + doc.setTextColor(40, 40, 40); + doc.text("Commit Activity", 14, currentY); + autoTable(doc, { + startY: currentY + 5, + head: [["Date", "Commits"]], + body: contribData.map((d) => [d.day, d.commits]), + styles: { fontSize: 10 }, + headStyles: { fillColor: [59, 130, 246] }, + }); + } - doc.text( - `Page ${i} of ${totalPages}`, - pageWidth - 30, - pageHeight - 10 - ); + doc.save("dashboard-metrics.pdf"); + } finally { + setIsExportingPDF(false); } + }; - doc.save("devtrack-profile-summary.pdf"); - } catch (error) { - - console.error("Failed to export PDF.", error); - } finally { - setIsExportingPDF(false); - } -}; return ( -
- +
- - - {isCopying - ? "Copying..." - : copied - ? "Copied!" - : "Copy Profile Summary"} - -
); } - -