Skip to content

Commit d7ecb90

Browse files
data download functionality in analytics dashboard (#201)
Co-authored-by: Ishaan Mittal <ishaanmittal123@gmail.com>
1 parent 7a9600b commit d7ecb90

File tree

5 files changed

+201
-22
lines changed

5 files changed

+201
-22
lines changed

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"tailwind-scrollbar-hide": "^2.0.0",
8181
"tailwindcss-animate": "^1.0.7",
8282
"xlsx": "^0.18.5",
83+
"xlsx-js-style": "^1.2.0",
8384
"yup": "^1.3.3"
8485
},
8586
"devDependencies": {

Diff for: src/components/dashboard/dashboard.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@ export default function Dashboard() {
107107
return Loading();
108108
}
109109

110+
console.log(seasonData);
110111
return (
111112
console.log(seasonData),
112113
<div>
113-
<Header currentView={currentView} onViewChange={setCurrentView} />
114+
<Header currentView={currentView} onViewChange={setCurrentView} data = {seasonData}/>
114115
<div className="flex h-screen overflow-y-auto bg-background">
115116
<main className="flex-1 overflow-y-auto p-6 no-scrollbar">
116117
<DataRibbon stats={seasonData} />

Diff for: src/components/dashboard/header.tsx

+27-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Download } from "lucide-react";
44
import { Button } from "@/components/ui/button";
55
import { cn } from "@/lib/utils";
6+
import { convertToCSV, convertToExcel } from "@/lib/csv-utils";
67

78
interface NavItem {
89
label: string;
@@ -18,9 +19,10 @@ interface ViewOption {
1819
interface NavHeaderProps {
1920
currentView: "reports" | "trends";
2021
onViewChange: (view: "reports" | "trends") => void;
22+
data?: any; // Add this prop for the data to be downloaded
2123
}
2224

23-
export function Header({ currentView, onViewChange }: NavHeaderProps) {
25+
export function Header({ currentView, onViewChange, data }: NavHeaderProps) {
2426
const navItems: NavItem[] = [
2527
{ label: "College Reports", href: "#", active: true },
2628
{ label: "Leaderboard", href: "#" },
@@ -32,6 +34,24 @@ export function Header({ currentView, onViewChange }: NavHeaderProps) {
3234
// { label: "Trends", value: "trends" },
3335
];
3436

37+
const handleDownload = () => {
38+
if (!data) return;
39+
40+
const blob = convertToExcel(data);
41+
if (!blob) return;
42+
43+
const url = URL.createObjectURL(blob);
44+
const link = document.createElement('a');
45+
46+
link.setAttribute('href', url);
47+
link.setAttribute('download', `placement-stats-${new Date().toISOString().split('T')[0]}.xlsx`);
48+
link.style.visibility = 'hidden';
49+
document.body.appendChild(link);
50+
link.click();
51+
document.body.removeChild(link);
52+
URL.revokeObjectURL(url);
53+
};
54+
3555
return (
3656
<div className="border-b mb-2">
3757
<div className="flex items-center justify-between px-6 pb-4">
@@ -55,7 +75,12 @@ export function Header({ currentView, onViewChange }: NavHeaderProps) {
5575
))}
5676
</div>
5777
<div>
58-
<Button variant="outline" size="icon" >
78+
<Button
79+
variant="outline"
80+
size="icon"
81+
onClick={handleDownload}
82+
disabled={!data}
83+
>
5984
<Download className="h-4 w-4" />
6085
</Button>
6186
</div>

Diff for: src/lib/csv-utils.ts

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as XLSX from 'xlsx';
2+
3+
export const convertToCSV = (data: any) => {
4+
if (!data || Object.keys(data).length === 0) return '';
5+
6+
// Handle nested objects by flattening them
7+
const flattenObject = (obj: any, prefix = '') => {
8+
return Object.keys(obj).reduce((acc: any, key: string) => {
9+
const pre = prefix.length ? prefix + '.' : '';
10+
if (typeof obj[key] === 'object' && obj[key] !== null) {
11+
Object.assign(acc, flattenObject(obj[key], pre + key));
12+
} else {
13+
acc[pre + key] = obj[key];
14+
}
15+
return acc;
16+
}, {});
17+
};
18+
19+
// Flatten the data
20+
const flatData = Object.keys(data).reduce((acc: any, key: string) => {
21+
acc[key] = typeof data[key] === 'object' ?
22+
flattenObject(data[key]) :
23+
data[key];
24+
return acc;
25+
}, {});
26+
27+
// Get all unique headers
28+
const headers = Array.from(new Set(
29+
Object.values(flatData)
30+
.flatMap(item => Object.keys(item))
31+
));
32+
33+
// Create CSV rows
34+
const csvRows = [
35+
headers.join(','), // Header row
36+
...Object.keys(flatData).map(key => {
37+
return headers.map(header => {
38+
const value = flatData[key][header] ?? '';
39+
return typeof value === 'string' ? `"${value}"` : value;
40+
}).join(',');
41+
})
42+
];
43+
44+
return csvRows.join('\n');
45+
};
46+
47+
export const convertToExcel = (data: any) => {
48+
if (!data || Object.keys(data).length === 0) return null;
49+
50+
const workbook = XLSX.utils.book_new();
51+
52+
// Function to flatten object for a sheet
53+
const flattenObject = (obj: any, prefix = '') => {
54+
return Object.keys(obj).reduce((acc: any, key: string) => {
55+
const pre = prefix.length ? prefix + '.' : '';
56+
if (typeof obj[key] === 'object' && obj[key] !== null) {
57+
Object.assign(acc, flattenObject(obj[key], pre + key));
58+
} else {
59+
acc[pre + key] = obj[key];
60+
}
61+
return acc;
62+
}, {});
63+
};
64+
65+
// Process each category of stats
66+
const categories = {
67+
'Overall Stats': data.overallStats,
68+
'Department Stats': data.departmentWiseStats,
69+
'Course Stats': data.courseWiseStats,
70+
'Gender Stats': data.genderWiseStats,
71+
'Category Stats': data.categoryWiseStats,
72+
'Academic Stats': data.academicWiseStats
73+
};
74+
75+
Object.entries(categories).forEach(([sheetName, statsData]) => {
76+
if (!statsData) return;
77+
78+
let flatData;
79+
80+
// Special handling for overall stats
81+
if (sheetName === 'Overall Stats') {
82+
flatData = Object.entries(statsData).map(([key, value]) => ({
83+
'Metric': key,
84+
'Value': value
85+
}));
86+
} else {
87+
// Handle other stats as before
88+
flatData = Object.entries(statsData).map(([key, value]) => ({
89+
Category: key,
90+
...flattenObject(value)
91+
}));
92+
}
93+
94+
// Create worksheet
95+
const worksheet = XLSX.utils.json_to_sheet(flatData);
96+
97+
// Add column width for better readability
98+
const colWidths = [
99+
{ wch: 30 }, // First column
100+
{ wch: 15 } // Second column
101+
];
102+
worksheet['!cols'] = colWidths;
103+
104+
// Add worksheet to workbook
105+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
106+
});
107+
108+
// Generate Excel file
109+
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
110+
return new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
111+
};

Diff for: yarn.lock

+60-19
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,14 @@ acorn@^8.12.0:
21012101
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
21022102
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
21032103

2104+
adler-32@~1.2.0:
2105+
version "1.2.0"
2106+
resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25"
2107+
integrity sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==
2108+
dependencies:
2109+
exit-on-epipe "~1.0.1"
2110+
printj "~1.1.0"
2111+
21042112
adler-32@~1.3.0:
21052113
version "1.3.1"
21062114
resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
@@ -2484,7 +2492,7 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.300016
24842492
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f"
24852493
integrity sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==
24862494

2487-
cfb@~1.2.1:
2495+
cfb@^1.1.4, cfb@~1.2.1:
24882496
version "1.2.2"
24892497
resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
24902498
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
@@ -2568,6 +2576,14 @@ cmdk@^0.2.0:
25682576
dependencies:
25692577
"@radix-ui/react-dialog" "1.0.0"
25702578

2579+
codepage@~1.14.0:
2580+
version "1.14.0"
2581+
resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99"
2582+
integrity sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==
2583+
dependencies:
2584+
commander "~2.14.1"
2585+
exit-on-epipe "~1.0.1"
2586+
25712587
codepage@~1.15.0:
25722588
version "1.15.0"
25732589
resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
@@ -2609,6 +2625,16 @@ commander@^4.0.0:
26092625
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
26102626
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
26112627

2628+
commander@~2.14.1:
2629+
version "2.14.1"
2630+
resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
2631+
integrity sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==
2632+
2633+
commander@~2.17.1:
2634+
version "2.17.1"
2635+
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
2636+
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
2637+
26122638
compute-scroll-into-view@^3.0.2:
26132639
version "3.1.0"
26142640
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
@@ -3360,6 +3386,11 @@ exenv@^1.2.0:
33603386
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
33613387
integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
33623388

3389+
exit-on-epipe@~1.0.1:
3390+
version "1.0.1"
3391+
resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
3392+
integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
3393+
33633394
export-to-csv@^1.3.0:
33643395
version "1.3.0"
33653396
resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-1.3.0.tgz#9f53cf61fcd6bed836683ed7d611767022d49919"
@@ -3413,6 +3444,11 @@ fastq@^1.6.0:
34133444
dependencies:
34143445
reusify "^1.0.4"
34153446

3447+
fflate@^0.3.8:
3448+
version "0.3.11"
3449+
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d"
3450+
integrity sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==
3451+
34163452
file-entry-cache@^8.0.0:
34173453
version "8.0.0"
34183454
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
@@ -4674,6 +4710,11 @@ pretty-format@^3.8.0:
46744710
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
46754711
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
46764712

4713+
printj@~1.1.0:
4714+
version "1.1.2"
4715+
resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
4716+
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
4717+
46774718
prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
46784719
version "15.8.1"
46794720
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -5602,16 +5643,7 @@ string-convert@^0.2.0:
56025643
resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
56035644
integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
56045645

5605-
"string-width-cjs@npm:string-width@^4.2.0":
5606-
version "4.2.3"
5607-
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
5608-
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
5609-
dependencies:
5610-
emoji-regex "^8.0.0"
5611-
is-fullwidth-code-point "^3.0.0"
5612-
strip-ansi "^6.0.1"
5613-
5614-
string-width@^4.1.0:
5646+
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
56155647
version "4.2.3"
56165648
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
56175649
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5683,14 +5715,7 @@ string.prototype.trimstart@^1.0.8:
56835715
define-properties "^1.2.1"
56845716
es-object-atoms "^1.0.0"
56855717

5686-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
5687-
version "6.0.1"
5688-
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
5689-
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
5690-
dependencies:
5691-
ansi-regex "^5.0.1"
5692-
5693-
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
5718+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
56945719
version "6.0.1"
56955720
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
56965721
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6172,6 +6197,22 @@ wrap-ansi@^8.1.0:
61726197
string-width "^5.0.1"
61736198
strip-ansi "^7.0.1"
61746199

6200+
xlsx-js-style@^1.2.0:
6201+
version "1.2.0"
6202+
resolved "https://registry.yarnpkg.com/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz#58455f2fd3c5e22807c2841f5b0631a07098b719"
6203+
integrity sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==
6204+
dependencies:
6205+
adler-32 "~1.2.0"
6206+
cfb "^1.1.4"
6207+
codepage "~1.14.0"
6208+
commander "~2.17.1"
6209+
crc-32 "~1.2.0"
6210+
exit-on-epipe "~1.0.1"
6211+
fflate "^0.3.8"
6212+
ssf "~0.11.2"
6213+
wmf "~1.0.1"
6214+
word "~0.3.0"
6215+
61756216
xlsx@^0.18.5:
61766217
version "0.18.5"
61776218
resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0"

0 commit comments

Comments
 (0)