Resume optimizer implemented with weekly counter/boosters/limits.

This commit is contained in:
Josh 2025-05-09 12:04:03 +00:00
parent 9fb03b0dc1
commit f3125b2145
5 changed files with 134 additions and 106 deletions

View File

@ -1667,29 +1667,38 @@ app.post(
} }
const userId = req.userId; const userId = req.userId;
const usageMonth = new Date().toISOString().slice(0, 7); const now = new Date();
const userPlanRow = await db.get(` const currentWeek = getWeekNumber(now); // Function defined below
SELECT is_premium, is_pro_premium
FROM user_profile
WHERE user_id = ?
`, [userId]);
let userPlan = 'basic'; // default const userProfile = await db.get(
if (userPlanRow?.is_pro_premium) userPlan = 'pro'; `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count
else if (userPlanRow?.is_premium) userPlan = 'premium'; FROM user_profile
WHERE user_id = ?`,
[userId]
);
let userPlan = 'basic';
if (userProfile?.is_pro_premium) userPlan = 'pro';
else if (userProfile?.is_premium) userPlan = 'premium';
if (userPlan === 'premium') { const weeklyLimits = { basic: 1, premium: 2, pro: 5 };
const usageRow = await db.get( const userWeeklyLimit = weeklyLimits[userPlan] || 0;
`SELECT usage_count FROM feature_usage
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`, let resetDate = new Date(userProfile.resume_limit_reset);
[userId, usageMonth] if (!userProfile.resume_limit_reset || now > resetDate) {
resetDate = new Date(now);
resetDate.setDate(now.getDate() + 7);
await db.run(
`UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`,
[resetDate.toISOString(), userId]
); );
const usageCount = usageRow?.usage_count || 0; userProfile.resume_optimizations_used = 0;
}
if (usageCount >= MAX_MONTHLY_REWRITES_PREMIUM) { const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0);
return res.status(403).json({ error: 'Monthly limit reached. Upgrade to Pro.' });
} if (userProfile.resume_optimizations_used >= totalLimit) {
return res.status(403).json({ error: 'Weekly resume optimization limit reached. Consider purchasing a booster pack.' });
} }
const filePath = req.file.path; const filePath = req.file.path;
@ -1715,29 +1724,16 @@ app.post(
const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || ''; const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || '';
if (userPlan === 'premium') { await db.run(
const existing = await db.get( `UPDATE user_profile SET resume_optimizations_used = resume_optimizations_used + 1 WHERE user_id = ?`,
`SELECT usage_count FROM feature_usage [userId]
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`, );
[userId, usageMonth]
); // Calculate remaining optimizations
if (existing) { const remainingOptimizations = totalLimit - (userProfile.resume_optimizations_used + 1);
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); fs.unlinkSync(filePath);
res.json({ optimizedResume }); res.json({ optimizedResume, remainingOptimizations });
} catch (err) { } catch (err) {
console.error('Error optimizing resume:', err); console.error('Error optimizing resume:', err);
res.status(500).json({ error: 'Failed to optimize resume.' }); res.status(500).json({ error: 'Failed to optimize resume.' });
@ -1745,6 +1741,58 @@ app.post(
} }
); );
app.get(
'/api/premium/resume/remaining',
authenticatePremiumUser,
async (req, res) => {
try {
const userId = req.userId;
const now = new Date();
const userProfile = await db.get(
`SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count
FROM user_profile
WHERE user_id = ?`,
[userId]
);
let userPlan = 'basic';
if (userProfile?.is_pro_premium) userPlan = 'pro';
else if (userProfile?.is_premium) userPlan = 'premium';
const weeklyLimits = { basic: 1, premium: 2, pro: 5 };
const userWeeklyLimit = weeklyLimits[userPlan] || 0;
let resetDate = new Date(userProfile.resume_limit_reset);
if (!userProfile.resume_limit_reset || now > resetDate) {
resetDate = new Date(now);
resetDate.setDate(now.getDate() + 7);
await db.run(
`UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`,
[resetDate.toISOString(), userId]
);
userProfile.resume_optimizations_used = 0;
}
const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0);
const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used;
res.json({ remainingOptimizations, resetDate });
} catch (err) {
console.error('Error fetching remaining optimizations:', err);
res.status(500).json({ error: 'Failed to fetch remaining optimizations.' });
}
}
);
// Helper function to get the week number
function getWeekNumber(date) {
const oneJan = new Date(date.getFullYear(), 0, 1);
const numberOfDays = Math.floor((date - oneJan) / (24 * 60 * 60 * 1000));
return Math.ceil((date.getDay() + 1 + numberOfDays) / 7);
}
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
FALLBACK (404 for unmatched routes) FALLBACK (404 for unmatched routes)

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
// If you still want file-saver + docx, import them here
function ResumeRewrite() { function ResumeRewrite() {
const [resumeFile, setResumeFile] = useState(null); const [resumeFile, setResumeFile] = useState(null);
@ -8,11 +7,31 @@ function ResumeRewrite() {
const [jobDescription, setJobDescription] = useState(''); const [jobDescription, setJobDescription] = useState('');
const [optimizedResume, setOptimizedResume] = useState(''); const [optimizedResume, setOptimizedResume] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [remainingOptimizations, setRemainingOptimizations] = useState(null);
const [resetDate, setResetDate] = useState(null);
const handleFileChange = (e) => { const handleFileChange = (e) => {
setResumeFile(e.target.files[0]); setResumeFile(e.target.files[0]);
}; };
const fetchRemainingOptimizations = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.get('/api/premium/resume/remaining', {
headers: { Authorization: `Bearer ${token}` },
});
setRemainingOptimizations(res.data.remainingOptimizations);
setResetDate(new Date(res.data.resetDate).toLocaleDateString());
} catch (err) {
console.error('Error fetching optimizations:', err);
setError('Could not fetch optimization limits.');
}
};
useEffect(() => {
fetchRemainingOptimizations();
}, []);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) { if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) {
@ -36,101 +55,62 @@ function ResumeRewrite() {
setOptimizedResume(res.data.optimizedResume || ''); setOptimizedResume(res.data.optimizedResume || '');
setError(''); setError('');
// Refresh remaining optimizations after optimizing
fetchRemainingOptimizations();
} catch (err) { } catch (err) {
console.error('Resume optimization error:', err); console.error('Resume optimization error:', err);
setError(err.response?.data?.error || 'Failed to optimize resume.'); setError(err.response?.data?.error || 'Failed to optimize resume.');
} }
}; };
// Optional: Download as docx
// const handleDownloadDocx = () => { ... }
return ( return (
<div className="max-w-4xl mx-auto mt-8 p-6 bg-white rounded shadow"> <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> <h2 className="text-2xl font-bold mb-4">Resume Optimizer</h2>
{remainingOptimizations !== null && (
<div className="mb-4 p-2 bg-blue-50 rounded border border-blue-200 text-blue-700">
<strong>{remainingOptimizations}</strong> Resume Optimizations Remaining This Week
{resetDate && <span className="ml-2">(Resets on {resetDate})</span>}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* File Upload */}
<div> <div>
<label className="block font-medium text-gray-700 mb-1"> <label className="block font-medium text-gray-700 mb-1">Upload Resume (PDF or DOCX):</label>
Upload Resume (PDF or DOCX): <input type="file" accept=".pdf,.docx" onChange={handleFileChange}
</label> 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"
<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> </div>
{/* Job Title */}
<div> <div>
<label className="block font-medium text-gray-700 mb-1"> <label className="block font-medium text-gray-700 mb-1">Job Title:</label>
Job Title: <input type="text" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)}
</label> className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
<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" placeholder="e.g., Software Engineer"
/> />
</div> </div>
{/* Job Description */}
<div> <div>
<label className="block font-medium text-gray-700 mb-1"> <label className="block font-medium text-gray-700 mb-1">Job Description:</label>
Job Description: <textarea value={jobDescription} onChange={(e) => setJobDescription(e.target.value)}
</label> rows={4} className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:ring-blue-200"
<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..." placeholder="Paste the job listing or requirements here..."
/> />
</div> </div>
{error && ( {error && <p className="text-red-600 font-semibold">{error}</p>}
<p className="text-red-600 font-semibold">
{error}
</p>
)}
<button <button type="submit"
type="submit" className="inline-block bg-blue-600 text-white font-semibold px-5 py-2 rounded hover:bg-blue-700 transition-colors">
className="inline-block bg-blue-600 text-white font-semibold
px-5 py-2 rounded hover:bg-blue-700
transition-colors"
>
Optimize Resume Optimize Resume
</button> </button>
</form> </form>
{/* Optimized Resume Display */}
{optimizedResume && ( {optimizedResume && (
<div className="mt-8"> <div className="mt-8">
<h3 className="text-xl font-bold mb-2">Optimized Resume</h3> <h3 className="text-xl font-bold mb-2">Optimized Resume</h3>
<pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border"> <pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border">{optimizedResume}</pre>
{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>
)} )}
</div> </div>

Binary file not shown.

Binary file not shown.

Binary file not shown.