Resume Builder added.
This commit is contained in:
parent
fbd8c97377
commit
9fb03b0dc1
@ -8,4 +8,5 @@ COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
|
|||||||
|
|
||||||
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_ENV=production
|
REACT_APP_ENV=production
|
||||||
REACT_APP_OPENAI_API_KEY=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
|
REACT_APP_OPENAI_API_KEY=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
|
||||||
|
OPENAI_API_KEY=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
|
@ -1,5 +1,5 @@
|
|||||||
// server3.js
|
// server3.js
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
@ -8,17 +8,27 @@ import sqlite3 from 'sqlite3';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import multer from 'multer';
|
||||||
|
import mammoth from 'mammoth';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
// --- Basic file init ---
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
const rootPath = path.resolve(__dirname, '..'); // Up one level
|
||||||
|
const env = process.env.NODE_ENV?.trim() || 'development';
|
||||||
|
const envPath = path.resolve(rootPath, `.env.${env}`);
|
||||||
|
dotenv.config({ path: envPath }); // Load .env file
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PREMIUM_PORT || 5002;
|
const PORT = process.env.PREMIUM_PORT || 5002;
|
||||||
|
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
const initDB = async () => {
|
const initDB = async () => {
|
||||||
try {
|
try {
|
||||||
@ -1607,6 +1617,135 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
RESUME OPTIMIZATION ENDPOINT
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||||
|
|
||||||
|
// Setup file upload via multer
|
||||||
|
const upload = multer({ dest: 'uploads/' });
|
||||||
|
|
||||||
|
// Basic usage gating config
|
||||||
|
const MAX_MONTHLY_REWRITES_PREMIUM = 2;
|
||||||
|
|
||||||
|
// Helper: build GPT prompt
|
||||||
|
const buildResumePrompt = (resumeText, jobTitle, jobDescription) => `
|
||||||
|
You are an expert resume writer specialized in precisely tailoring existing resumes for optimal ATS compatibility and explicit alignment with provided job descriptions.
|
||||||
|
|
||||||
|
STRICT GUIDELINES:
|
||||||
|
1. DO NOT invent any new job titles, employers, dates, locations, compensation details, or roles not explicitly stated in the user's original resume.
|
||||||
|
2. Creatively but realistically reframe, reposition, and explicitly recontextualize the user's existing professional experiences and skills to clearly demonstrate alignment with the provided job description.
|
||||||
|
3. Emphasize transferable skills, tasks, and responsibilities from the user's provided resume content that directly match the requirements and responsibilities listed in the job description.
|
||||||
|
4. Clearly and explicitly incorporate exact keywords, responsibilities, skills, and competencies directly from the provided job description.
|
||||||
|
5. Minimize or entirely remove irrelevant technical jargon or specific software names not directly aligned with the job description.
|
||||||
|
6. Avoid generic résumé clichés (e.g., "results-driven," "experienced professional," "dedicated leader," "dynamic professional," etc.).
|
||||||
|
7. NEVER directly reuse specific details such as salary information, compensation, or other company-specific information from the provided job description.
|
||||||
|
|
||||||
|
Target Job Title:
|
||||||
|
${jobTitle}
|
||||||
|
|
||||||
|
Provided Job Description:
|
||||||
|
${jobDescription}
|
||||||
|
|
||||||
|
User's Original Resume:
|
||||||
|
${resumeText}
|
||||||
|
|
||||||
|
Precisely Tailored, ATS-Optimized Resume:
|
||||||
|
`;
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/api/premium/resume/optimize',
|
||||||
|
upload.single('resumeFile'),
|
||||||
|
authenticatePremiumUser,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { jobTitle, jobDescription } = req.body;
|
||||||
|
if (!jobTitle || !jobDescription || !req.file) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.userId;
|
||||||
|
const usageMonth = new Date().toISOString().slice(0, 7);
|
||||||
|
const userPlanRow = await db.get(`
|
||||||
|
SELECT is_premium, is_pro_premium
|
||||||
|
FROM user_profile
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, [userId]);
|
||||||
|
|
||||||
|
let userPlan = 'basic'; // default
|
||||||
|
if (userPlanRow?.is_pro_premium) userPlan = 'pro';
|
||||||
|
else if (userPlanRow?.is_premium) userPlan = 'premium';
|
||||||
|
|
||||||
|
|
||||||
|
if (userPlan === 'premium') {
|
||||||
|
const usageRow = await db.get(
|
||||||
|
`SELECT usage_count FROM feature_usage
|
||||||
|
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
|
||||||
|
[userId, usageMonth]
|
||||||
|
);
|
||||||
|
const usageCount = usageRow?.usage_count || 0;
|
||||||
|
|
||||||
|
if (usageCount >= MAX_MONTHLY_REWRITES_PREMIUM) {
|
||||||
|
return res.status(403).json({ error: 'Monthly limit reached. Upgrade to Pro.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = req.file.path;
|
||||||
|
const fileExt = req.file.originalname.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
let resumeText = '';
|
||||||
|
if (fileExt === 'pdf') {
|
||||||
|
resumeText = await extractTextFromPDF(filePath);
|
||||||
|
} else if (fileExt === 'docx') {
|
||||||
|
const result = await mammoth.extractRawText({ path: filePath });
|
||||||
|
resumeText = result.value;
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Unsupported file type.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = buildResumePrompt(resumeText, jobTitle, jobDescription);
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-4-turbo',
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || '';
|
||||||
|
|
||||||
|
if (userPlan === 'premium') {
|
||||||
|
const existing = await db.get(
|
||||||
|
`SELECT usage_count FROM feature_usage
|
||||||
|
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
|
||||||
|
[userId, usageMonth]
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
await db.run(
|
||||||
|
`UPDATE feature_usage SET usage_count = usage_count + 1
|
||||||
|
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
|
||||||
|
[userId, usageMonth]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await db.run(
|
||||||
|
`INSERT INTO feature_usage (user_id, feature_name, usage_month, usage_count)
|
||||||
|
VALUES (?, 'resume_optimize', ?, 1)`,
|
||||||
|
[userId, usageMonth]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
res.json({ optimizedResume });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error optimizing resume:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to optimize resume.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
FALLBACK (404 for unmatched routes)
|
FALLBACK (404 for unmatched routes)
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
2832
package-lock.json
generated
2832
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,12 +17,19 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cra-template": "1.2.0",
|
"cra-template": "1.2.0",
|
||||||
|
"docx": "^9.5.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
|
"mammoth": "^1.9.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
"multer": "^1.4.5-lts.2",
|
||||||
|
"openai": "^4.97.0",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
|
"pdfjs-dist": "^3.11.174",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
45
src/App.js
45
src/App.js
@ -8,7 +8,7 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
// Import all components from the components folder
|
// Import all components
|
||||||
import PremiumRoute from './components/PremiumRoute.js';
|
import PremiumRoute from './components/PremiumRoute.js';
|
||||||
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
||||||
import GettingStarted from './components/GettingStarted.js';
|
import GettingStarted from './components/GettingStarted.js';
|
||||||
@ -23,6 +23,9 @@ import Paywall from './components/Paywall.js';
|
|||||||
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
||||||
import MultiScenarioView from './components/MultiScenarioView.js';
|
import MultiScenarioView from './components/MultiScenarioView.js';
|
||||||
|
|
||||||
|
// 1) Import your ResumeRewrite component
|
||||||
|
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -31,15 +34,20 @@ function App() {
|
|||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// If you want Resume Optimizer to be considered a "premium path" too:
|
||||||
const premiumPaths = [
|
const premiumPaths = [
|
||||||
'/milestone-tracker',
|
'/milestone-tracker',
|
||||||
'/paywall',
|
'/paywall',
|
||||||
'/financial-profile',
|
'/financial-profile',
|
||||||
'/multi-scenario',
|
'/multi-scenario',
|
||||||
'/premium-onboarding',
|
'/premium-onboarding',
|
||||||
|
'/resume-optimizer', // add it here if you want
|
||||||
];
|
];
|
||||||
const showPremiumCTA = !premiumPaths.includes(location.pathname);
|
const showPremiumCTA = !premiumPaths.includes(location.pathname);
|
||||||
|
|
||||||
|
// 2) We'll define "canAccessPremium" to handle *both* is_premium or is_pro_premium
|
||||||
|
const canAccessPremium = user?.is_premium || user?.is_pro_premium;
|
||||||
|
|
||||||
// Rehydrate user if there's a token
|
// Rehydrate user if there's a token
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@ -134,7 +142,7 @@ function App() {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* Premium sections (greyed out if not is_premium) */}
|
{/* Premium sections (still checking only user?.is_premium for these) */}
|
||||||
<li>
|
<li>
|
||||||
{user?.is_premium ? (
|
{user?.is_premium ? (
|
||||||
<Link
|
<Link
|
||||||
@ -207,6 +215,25 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{/* 3) A new link for Resume Optimizer that checks canAccessPremium */}
|
||||||
|
<li>
|
||||||
|
{canAccessPremium ? (
|
||||||
|
<Link
|
||||||
|
className="text-blue-600 hover:text-blue-800"
|
||||||
|
to="/resume-optimizer"
|
||||||
|
>
|
||||||
|
Resume Optimizer
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 cursor-not-allowed">
|
||||||
|
Resume Optimizer{' '}
|
||||||
|
<span className="text-green-600">
|
||||||
|
(Premium or Pro Only)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
|
||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@ -221,8 +248,8 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* "Upgrade to Premium" button if not premium and on a free path */}
|
{/* "Upgrade to Premium" button if not premium/pro and on a free path */}
|
||||||
{showPremiumCTA && isAuthenticated && !user?.is_premium && (
|
{showPremiumCTA && isAuthenticated && !canAccessPremium && (
|
||||||
<button
|
<button
|
||||||
className="rounded bg-blue-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
className="rounded bg-blue-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
onClick={() => navigate('/paywall')}
|
onClick={() => navigate('/paywall')}
|
||||||
@ -291,6 +318,16 @@ function App() {
|
|||||||
</PremiumRoute>
|
</PremiumRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 4) The new Resume Optimizer route */}
|
||||||
|
<Route
|
||||||
|
path="/resume-optimizer"
|
||||||
|
element={
|
||||||
|
<PremiumRoute user={user}>
|
||||||
|
<ResumeRewrite />
|
||||||
|
</PremiumRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// src/components/CareerSelectDropdown.js
|
// src/components/CareerSelectDropdown.js
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => {
|
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => {
|
||||||
const fetchMilestones = (careerPathId) => {
|
const fetchMilestones = (careerPathId) => {
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
|
|
||||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||||
import PopoutPanel from './PopoutPanel.js';
|
import PopoutPanel from './PopoutPanel.js';
|
||||||
import MilestoneTracker from './MilestoneTracker.js';
|
|
||||||
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
||||||
import Chatbot from './Chatbot.js';
|
import Chatbot from './Chatbot.js';
|
||||||
|
|
||||||
@ -150,7 +149,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
// Show session expired modal
|
// Show session expired modal
|
||||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||||
const [sessionHandled, setSessionHandled] = useState(false);
|
|
||||||
|
|
||||||
// ============= NEW State =============
|
// ============= NEW State =============
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
|
@ -490,7 +490,6 @@ export default function MilestoneTimeline({
|
|||||||
// 9) Render
|
// 9) Render
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Combine "Career" + "Financial" if you want them in a single list:
|
// Combine "Career" + "Financial" if you want them in a single list:
|
||||||
const allMilestones = [...milestones.Career, ...milestones.Financial];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-timeline" style={{ padding: '1rem' }}>
|
<div className="milestone-timeline" style={{ padding: '1rem' }}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ClipLoader } from "react-spinners";
|
import { ClipLoader } from "react-spinners";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import LoanRepayment from "./LoanRepayment.js";
|
import LoanRepayment from "./LoanRepayment.js";
|
||||||
import "./PopoutPanel.css"; // You can keep or remove depending on your needs
|
import "./PopoutPanel.css"; // You can keep or remove depending on your needs
|
||||||
|
|
||||||
|
@ -7,12 +7,14 @@ function PremiumRoute({ user, children }) {
|
|||||||
return <Navigate to="/signin" replace />;
|
return <Navigate to="/signin" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.is_premium) {
|
// Check if user has *either* premium or pro
|
||||||
// Logged in but not premium; go to paywall
|
const hasPremiumOrPro = user.is_premium || user.is_pro_premium;
|
||||||
|
if (!hasPremiumOrPro) {
|
||||||
|
// Logged in but neither plan; go to paywall
|
||||||
return <Navigate to="/paywall" replace />;
|
return <Navigate to="/paywall" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User is logged in and has premium
|
// User is logged in and has premium or pro
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
140
src/components/ResumeRewrite.js
Normal file
140
src/components/ResumeRewrite.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
// If you still want file-saver + docx, import them here
|
||||||
|
|
||||||
|
function ResumeRewrite() {
|
||||||
|
const [resumeFile, setResumeFile] = useState(null);
|
||||||
|
const [jobTitle, setJobTitle] = useState('');
|
||||||
|
const [jobDescription, setJobDescription] = useState('');
|
||||||
|
const [optimizedResume, setOptimizedResume] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
setResumeFile(e.target.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) {
|
||||||
|
setError('Please fill in all fields.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('resumeFile', resumeFile);
|
||||||
|
formData.append('jobTitle', jobTitle);
|
||||||
|
formData.append('jobDescription', jobDescription);
|
||||||
|
|
||||||
|
const res = await axios.post('/api/premium/resume/optimize', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setOptimizedResume(res.data.optimizedResume || '');
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Resume optimization error:', err);
|
||||||
|
setError(err.response?.data?.error || 'Failed to optimize resume.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional: Download as docx
|
||||||
|
// const handleDownloadDocx = () => { ... }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto mt-8 p-6 bg-white rounded shadow">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Resume Optimizer</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* File Upload */}
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">
|
||||||
|
Upload Resume (PDF or DOCX):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.docx"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="file:mr-4 file:py-2 file:px-4
|
||||||
|
file:border-0 file:text-sm file:font-semibold
|
||||||
|
file:bg-blue-50 file:text-blue-700
|
||||||
|
hover:file:bg-blue-100
|
||||||
|
cursor-pointer
|
||||||
|
text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Title */}
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">
|
||||||
|
Job Title:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={jobTitle}
|
||||||
|
onChange={(e) => setJobTitle(e.target.value)}
|
||||||
|
className="w-full border rounded px-3 py-2 focus:outline-none
|
||||||
|
focus:ring focus:ring-blue-200"
|
||||||
|
placeholder="e.g., Software Engineer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Description */}
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">
|
||||||
|
Job Description:
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={jobDescription}
|
||||||
|
onChange={(e) => setJobDescription(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="w-full border rounded px-3 py-2 focus:outline-none
|
||||||
|
focus:ring focus:ring-blue-200"
|
||||||
|
placeholder="Paste the job listing or requirements here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-600 font-semibold">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-block bg-blue-600 text-white font-semibold
|
||||||
|
px-5 py-2 rounded hover:bg-blue-700
|
||||||
|
transition-colors"
|
||||||
|
>
|
||||||
|
Optimize Resume
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Optimized Resume Display */}
|
||||||
|
{optimizedResume && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-xl font-bold mb-2">Optimized Resume</h3>
|
||||||
|
<pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border">
|
||||||
|
{optimizedResume}
|
||||||
|
</pre>
|
||||||
|
{/*
|
||||||
|
Optional Download Button
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadDocx}
|
||||||
|
className="mt-2 inline-block bg-green-600 text-white font-semibold
|
||||||
|
px-4 py-2 rounded hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
Download as DOCX
|
||||||
|
</button>
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResumeRewrite;
|
BIN
uploads/956f53dfb89ce1ad0f42f9262cd79ca3
Normal file
BIN
uploads/956f53dfb89ce1ad0f42f9262cd79ca3
Normal file
Binary file not shown.
BIN
uploads/fc674b7af2f80e12e5a618b0a67dcbb4
Normal file
BIN
uploads/fc674b7af2f80e12e5a618b0a67dcbb4
Normal file
Binary file not shown.
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user