From 39e13a671dcde442ad6932b4d1e34e60afdb92c6 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Mon, 9 Sep 2024 23:27:43 -0500 Subject: [PATCH] enable recruiter mass resume downloads (#44) * enable recruiter resume downloads * dont keep @illinois.edu in filenames * fix linting issues * fix prettier --- clientv2/package.json | 3 + .../src/components/SearchProfiles/Results.tsx | 76 +++++++++++- clientv2/yarn.lock | 115 +++++++++++++++++- 3 files changed, 187 insertions(+), 7 deletions(-) diff --git a/clientv2/package.json b/clientv2/package.json index 5adb9bf..6b9441f 100644 --- a/clientv2/package.json +++ b/clientv2/package.json @@ -28,6 +28,8 @@ "@ungap/with-resolvers": "^0.1.0", "axios": "^1.7.2", "dotenv": "^16.4.5", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", "pdfjs-dist": "^4.5.136", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -48,6 +50,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/file-saver": "^2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.13.1", diff --git a/clientv2/src/components/SearchProfiles/Results.tsx b/clientv2/src/components/SearchProfiles/Results.tsx index 4dc7db0..18b5d9f 100644 --- a/clientv2/src/components/SearchProfiles/Results.tsx +++ b/clientv2/src/components/SearchProfiles/Results.tsx @@ -13,8 +13,13 @@ import { } from '@mantine/core'; import { IconQuestionMark } from '@tabler/icons-react'; import { notifications } from '@mantine/notifications'; +import JSZip from 'jszip'; +import { saveAs } from 'file-saver'; import { DegreeLevel } from '../ProfileViewer/options'; import { ViewStudentProfile } from '@/pages/recruiter/ViewStudentProfile.page'; +import { useApi } from '@/util/api'; + +const MAX_RESUMES_DOWNLAOD = 2000; export interface ProfileSearchDegreeEntry { level: DegreeLevel; @@ -39,6 +44,7 @@ export const ProfileSearchResults: React.FC = ({ data const [selectedUsername, setSelectedUsername] = useState(null); const [activePage, setActivePage] = useState(1); const itemsPerPage = 10; + const api = useApi(); if (data === null) { return null; @@ -68,13 +74,73 @@ export const ProfileSearchResults: React.FC = ({ data setSelectedUsername(username); setModalOpened(true); }; - const notImplError = () => { + const massDownloadErrorNotification = (numErrored?: number, partial: boolean = false) => { + notifications.show({ + title: `Error downloading ${partial ? 'some' : ''} resumes`, + color: numErrored ? 'yellow' : 'red', + message: `There was an error downloading ${numErrored ? numErrored.toString() : 'the selected'} resumes.`, + }); + }; + const massDownloadSuccessNotification = (numSuccess: number) => { notifications.show({ - color: 'red', - title: 'Not Implemented Yet', - message: 'This feature still in the works. Check back later.', + title: 'Downloaded resumes', + color: 'blue', + message: `Successfully downloaded ${numSuccess} resumes.`, }); }; + + const downloadResumes = async () => { + if (selectedRows.length > MAX_RESUMES_DOWNLAOD) { + return notifications.show({ + title: 'Error downloading resumes', + color: 'red', + message: `You cannot download more than ${MAX_RESUMES_DOWNLAOD} in one request.`, + }); + } + let urls: string[]; + try { + urls = (await api.post('/recruiter/mass_download', { usernames: selectedRows })).data; + } catch (e) { + return massDownloadErrorNotification(); + } + let numError = 0; + let numSuccess = 0; + const urlMapper: Record = {}; + for (let i = 0; i < urls.length; i++) { + urlMapper[urls[i]] = selectedRows[i]; + } + const allPromises = await Promise.allSettled(urls.map((x) => ({ url: x, promise: fetch(x) }))); + const realBlobs = []; + for (const outerPromise of allPromises) { + if ( + outerPromise.status === 'fulfilled' && + (await outerPromise.value.promise).status === 200 + ) { + numSuccess += 1; + realBlobs.push({ + blob: (await outerPromise.value.promise).blob(), + filename: `${urlMapper[outerPromise.value.url].replace('@illinois.edu', '')}.pdf`, + }); + } else { + numError += 1; + } + } + if (numError > 0) { + massDownloadErrorNotification(numError, !(numSuccess === 0)); + } + if (numSuccess === 0) return [numSuccess, numError]; + const zip = new JSZip(); + const yourDate = new Date().toISOString().split('T')[0]; + const folderName = `ACM_UIUC_Resumes-${yourDate}`; + for (const { blob, filename } of realBlobs) { + zip.file(`${folderName}/${filename}`, blob); + } + const zipContent = await zip.generateAsync({ type: 'blob' }); + + saveAs(zipContent, `${folderName}.zip`); + massDownloadSuccessNotification(numSuccess); + return [numSuccess, numError]; + }; const handleRowSelect = (id: string) => { setSelectedRows((prevSelectedRows) => prevSelectedRows.includes(id) @@ -131,7 +197,7 @@ export const ProfileSearchResults: React.FC = ({ data