From 5959c7d552e0dec276d8565aa972b695570c0632 Mon Sep 17 00:00:00 2001 From: DUDHAT HEMIL PRAVINKUMAR Date: Thu, 30 Oct 2025 00:08:58 +0530 Subject: [PATCH] Enhance: Add keyboard shortcuts and improve file upload and download button functionality --- src/App.jsx | 62 +++++- src/Components/DownloadButton.jsx | 69 ++++--- src/Components/EnhancementViewer.jsx | 292 +++++++++++++++++---------- src/Components/FileUpload.jsx | 34 ++-- 4 files changed, 305 insertions(+), 152 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c3f5f3e..9b71616 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,18 +1,19 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import EliteHeader from "./Components/EliteHeader"; import FileUpload from "./Components/FileUpload"; import EnhancementViewer from "./Components/EnhancementViewer"; import DownloadButton from "./Components/DownloadButton"; -import './i18n'; - - - +import "./i18n"; export default function App() { const [resumeFile, setResumeFile] = useState(null); const [enhancedText, setEnhancedText] = useState(""); const [base64FileContent, setBase64FileContent] = useState(""); const [fileFormat, setFileFormat] = useState(""); + // Refs for triggering actions + const uploadButtonRef = useRef(null); + const enhanceButtonRef = useRef(null); + const downloadButtonRef = useRef(null); // New central states for quota error handling const [errorData, setErrorData] = useState({ @@ -36,6 +37,38 @@ export default function App() { } }, [resetTimer, isButtonDisabled]); + // Keyboard shortcuts: Ctrl+U (Upload), Ctrl+E (Enhance), Ctrl+D (Download) + useEffect(() => { + function handleKeyDown(e) { + if (e.ctrlKey && !e.shiftKey && !e.altKey) { + // Prevent browser default for these keys + if (["u", "e", "d"].includes(e.key.toLowerCase())) { + e.preventDefault(); + } + if (e.key.toLowerCase() === "u" && !resumeFile) { + // Focus or click upload + if (uploadButtonRef.current) uploadButtonRef.current.click(); + } else if ( + e.key.toLowerCase() === "e" && + resumeFile && + enhanceButtonRef.current + ) { + enhanceButtonRef.current.click(); + } else if ( + e.key.toLowerCase() === "d" && + resumeFile && + enhancedText && + base64FileContent && + downloadButtonRef.current + ) { + downloadButtonRef.current.click(); + } + } + } + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [resumeFile, enhancedText, base64FileContent]); + // Quota lock visual animation const QuotaErrorBanner = () => errorData.isQuota ? ( @@ -53,10 +86,14 @@ export default function App() {
- +
{!resumeFile ? ( - + ) : (
{/* Reset button for mobile */} @@ -69,7 +106,7 @@ export default function App() { }} className="text-sm text-blue-600 hover:text-blue-800 underline mb-2" > - ← Upload different file +  Upload different file - + {enhancedText && base64FileContent && ( )}
)}
- + diff --git a/src/Components/DownloadButton.jsx b/src/Components/DownloadButton.jsx index 4d7632e..77e3059 100644 --- a/src/Components/DownloadButton.jsx +++ b/src/Components/DownloadButton.jsx @@ -1,10 +1,12 @@ import { useTranslation } from "react-i18next"; -export default function DownloadButton({ +export default function DownloadButton({ enhancedFileBase64, - fileFormat + fileFormat, + downloadButtonRef, + shortcutLabel, }) { - const { t } = useTranslation(); + const { t } = useTranslation(); // Helper to convert base64 to Blob and trigger download const downloadFileFromBase64 = (base64Data, fileName, mimeType) => { @@ -17,7 +19,7 @@ export default function DownloadButton({ const blob = new Blob([byteArray], { type: mimeType }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); @@ -35,7 +37,8 @@ export default function DownloadButton({ mimeType = "application/pdf"; break; case "docx": - mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + mimeType = + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; break; case "txt": mimeType = "text/plain"; @@ -43,34 +46,46 @@ export default function DownloadButton({ default: mimeType = "application/octet-stream"; } - downloadFileFromBase64(enhancedFileBase64, `enhanced_resume.${ext}`, mimeType); + downloadFileFromBase64( + enhancedFileBase64, + `enhanced_resume.${ext}`, + mimeType + ); } else { - alert(t("download.noFile")); + alert(t("download.noFile")); } }; return (
- + + + + {t("download.button")} + + {shortcutLabel && ( + + {shortcutLabel} + + )} +
); -} \ No newline at end of file +} diff --git a/src/Components/EnhancementViewer.jsx b/src/Components/EnhancementViewer.jsx index a3db1c8..3db04d5 100644 --- a/src/Components/EnhancementViewer.jsx +++ b/src/Components/EnhancementViewer.jsx @@ -14,16 +14,16 @@ function cleanResumeText(input) { .replace(/\*/g, "") .replace(/^[=#]+\s*(.*)$/gm, "$1") .replace(/^\s*=\s*$/gm, "") - .split('\n') - .map(line => + .split("\n") + .map((line) => line - .replace(/^#+\s*/, '') - .replace(/^=+\s*/, '') - .replace(/\s*=+\s*$/, '') + .replace(/^#+\s*/, "") + .replace(/^=+\s*/, "") + .replace(/\s*=+\s*$/, "") .trim() ) - .filter(line => line.trim().length > 0) - .join('\n') + .filter((line) => line.trim().length > 0) + .join("\n") .trim(); } @@ -32,23 +32,29 @@ function extractATSScores(text) { const scores = { original: null, enhanced: null, - improvements: [] + improvements: [], }; // Match patterns like "Original ATS Score: 65/100" or "Score: 65" - const originalMatch = text.match(/Original\s+ATS\s+Score:\s*(\d+)(?:\/100)?/i); - const enhancedMatch = text.match(/Enhanced\s+ATS\s+Score:\s*(\d+)(?:\/100)?/i); - + const originalMatch = text.match( + /Original\s+ATS\s+Score:\s*(\d+)(?:\/100)?/i + ); + const enhancedMatch = text.match( + /Enhanced\s+ATS\s+Score:\s*(\d+)(?:\/100)?/i + ); + if (originalMatch) scores.original = parseInt(originalMatch[1]); if (enhancedMatch) scores.enhanced = parseInt(enhancedMatch[1]); // Extract improvements section - const improvementsMatch = text.match(/Key Improvements?:([\s\S]*?)(?=###|$)/i); + const improvementsMatch = text.match( + /Key Improvements?:([\s\S]*?)(?=###|$)/i + ); if (improvementsMatch) { scores.improvements = improvementsMatch[1] - .split('\n') - .map(line => line.replace(/^[\s\-\*]+/, '').trim()) - .filter(line => line.length > 0); + .split("\n") + .map((line) => line.replace(/^[\s\-\*]+/, "").trim()) + .filter((line) => line.length > 0); } return scores; @@ -57,18 +63,25 @@ function extractATSScores(text) { // Progress bar component for ATS score function ATSScoreBar({ score, label }) { if (score === null || score === undefined) return null; - + const percentage = Math.min(Math.max(score, 0), 100); - const color = percentage >= 80 ? 'bg-green-500' : percentage >= 60 ? 'bg-yellow-500' : 'bg-red-500'; - + const color = + percentage >= 80 + ? "bg-green-500" + : percentage >= 60 + ? "bg-yellow-500" + : "bg-red-500"; + return (
{label} - {percentage}/100 + + {percentage}/100 +
-
@@ -77,18 +90,20 @@ function ATSScoreBar({ score, label }) { ); } -export default function EnhancementViewer({ - resumeFile, - setEnhancedText, - setBase64FileContent, - setFileFormat +export default function EnhancementViewer({ + resumeFile, + setEnhancedText, + setBase64FileContent, + setFileFormat, + enhanceButtonRef, + shortcutLabel, }) { const [result, setResult] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [atsScores, setAtsScores] = useState(null); - const { t, i18n } = useTranslation(); + const { t, i18n } = useTranslation(); useEffect(() => { if (!resumeFile) return; @@ -101,15 +116,21 @@ export default function EnhancementViewer({ try { const { text: resumeText, format } = await extractText(resumeFile); - + if (!resumeText || resumeText.trim() === "") { throw new Error(t("errors.noText")); } - const uploaderName = resumeFile.name.replace(/\.[^/.]+$/, "") || "Resume"; - const language = i18n.language || 'en'; - - const enhanced = await enhanceResumeWithGemini(resumeText, format, uploaderName, language); + const uploaderName = + resumeFile.name.replace(/\.[^/.]+$/, "") || "Resume"; + const language = i18n.language || "en"; + + const enhanced = await enhanceResumeWithGemini( + resumeText, + format, + uploaderName, + language + ); // Extract ATS scores from full response const scores = extractATSScores(enhanced.displayText); @@ -122,7 +143,6 @@ export default function EnhancementViewer({ setEnhancedText(cleanDisplay); setBase64FileContent(enhanced.base64); setFileFormat(enhanced.format); - } catch (e) { console.error("Enhancement error:", e); setError(e.message || t("errors.generic")); @@ -130,54 +150,104 @@ export default function EnhancementViewer({ setLoading(false); } })(); - }, [resumeFile, setEnhancedText, setBase64FileContent, setFileFormat, t, i18n.language]); + }, [ + resumeFile, + setEnhancedText, + setBase64FileContent, + setFileFormat, + t, + i18n.language, + ]); // Helper to render formatted text with better mobile support const renderFormattedText = (text) => { - return text.split('\n').map((line, idx) => { - const trimmedLine = line.trim(); - - // Skip ATS score lines (already displayed separately) - if (trimmedLine.toLowerCase().includes('score:') || - trimmedLine.toLowerCase().includes('ats analysis') || - trimmedLine.toLowerCase().includes('key improvement')) { - return null; - } + return text + .split("\n") + .map((line, idx) => { + const trimmedLine = line.trim(); - // Section headers - if (trimmedLine === trimmedLine.toUpperCase() && trimmedLine.length > 0 && trimmedLine.length < 50) { - return ( -

- {trimmedLine} -

- ); - } + // Skip ATS score lines (already displayed separately) + if ( + trimmedLine.toLowerCase().includes("score:") || + trimmedLine.toLowerCase().includes("ats analysis") || + trimmedLine.toLowerCase().includes("key improvement") + ) { + return null; + } - // Explanation text - if (trimmedLine.startsWith('Explanation:')) { + // Section headers + if ( + trimmedLine === trimmedLine.toUpperCase() && + trimmedLine.length > 0 && + trimmedLine.length < 50 + ) { + return ( +

+ {trimmedLine} +

+ ); + } + + // Explanation text + if (trimmedLine.startsWith("Explanation:")) { + return ( +

+ {trimmedLine} +

+ ); + } + + // Empty lines + if (!trimmedLine) { + return
; + } + + // Regular text with better mobile wrapping return ( -

+

{trimmedLine}

); - } - - // Empty lines - if (!trimmedLine) { - return
; - } - - // Regular text with better mobile wrapping - return ( -

- {trimmedLine} -

- ); - }).filter(Boolean); + }) + .filter(Boolean); }; + // Add Enhance Resume button (if not loading, not error, and resumeFile exists) return (
+ {/* Enhance Resume Button */} + {!loading && !error && resumeFile && ( +
+ + {shortcutLabel && ( + + {shortcutLabel} + + )} +
+ )} + {loading && (
@@ -192,7 +262,9 @@ export default function EnhancementViewer({ {error && (
-

{t("errors.title")}

+

+ {t("errors.title")} +

{error}

)} @@ -204,42 +276,58 @@ export default function EnhancementViewer({ {/* ATS Score Display */} - {atsScores && (atsScores.original !== null || atsScores.enhanced !== null) && ( -
-

ATS Score Analysis

- -
- {atsScores.original !== null && ( - + {atsScores && + (atsScores.original !== null || atsScores.enhanced !== null) && ( +
+

+ ATS Score Analysis +

+ +
+ {atsScores.original !== null && ( + + )} + + {atsScores.enhanced !== null && ( + + )} +
+ + {/* Score improvement badge */} + {atsScores.original !== null && atsScores.enhanced !== null && ( +
+

+ ✨ Improvement: +{atsScores.enhanced - atsScores.original}{" "} + points +

+
)} - - {atsScores.enhanced !== null && ( - + + {/* Key improvements */} + {atsScores.improvements.length > 0 && ( +
+

+ Key Improvements: +

+
    + {atsScores.improvements + .slice(0, 5) + .map((improvement, idx) => ( +
  • + {improvement} +
  • + ))} +
+
)}
- - {/* Score improvement badge */} - {atsScores.original !== null && atsScores.enhanced !== null && ( -
-

- ✨ Improvement: +{atsScores.enhanced - atsScores.original} points -

-
- )} - - {/* Key improvements */} - {atsScores.improvements.length > 0 && ( -
-

Key Improvements:

-
    - {atsScores.improvements.slice(0, 5).map((improvement, idx) => ( -
  • {improvement}
  • - ))} -
-
- )} -
- )} + )} {/* Enhanced Resume Content */}
@@ -249,4 +337,4 @@ export default function EnhancementViewer({ )}
); -} \ No newline at end of file +} diff --git a/src/Components/FileUpload.jsx b/src/Components/FileUpload.jsx index 27d6996..81ea10e 100644 --- a/src/Components/FileUpload.jsx +++ b/src/Components/FileUpload.jsx @@ -1,7 +1,11 @@ -import { useRef, useState } from "react"; +import { useRef, useState, forwardRef } from "react"; import { useTranslation } from "react-i18next"; -export default function FileUpload({ setResumeFile }) { +export default function FileUpload({ + setResumeFile, + uploadButtonRef, + shortcutLabel, +}) { const inputRef = useRef(); const [dragActive, setDragActive] = useState(false); const { t } = useTranslation(); @@ -42,18 +46,24 @@ export default function FileUpload({ setResumeFile }) { > 📄

- {t("upload.title")}{" "} - PDF,{" "} + {t("upload.title")} PDF,{" "} DOCX {t("upload.or")}{" "} - TXT{" "} - {t("upload.here")} + TXT {t("upload.here")}

- +
+ + {shortcutLabel && ( + + {shortcutLabel} + + )} +