Resume optimizer implemented with weekly counter/boosters/limits.
This commit is contained in:
parent
bceb5591f0
commit
49b03eb083
@ -1667,29 +1667,38 @@ app.post(
|
||||
}
|
||||
|
||||
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]);
|
||||
const now = new Date();
|
||||
const currentWeek = getWeekNumber(now); // Function defined below
|
||||
|
||||
let userPlan = 'basic'; // default
|
||||
if (userPlanRow?.is_pro_premium) userPlan = 'pro';
|
||||
else if (userPlanRow?.is_premium) userPlan = 'premium';
|
||||
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';
|
||||
|
||||
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 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]
|
||||
);
|
||||
const usageCount = usageRow?.usage_count || 0;
|
||||
userProfile.resume_optimizations_used = 0;
|
||||
}
|
||||
|
||||
if (usageCount >= MAX_MONTHLY_REWRITES_PREMIUM) {
|
||||
return res.status(403).json({ error: 'Monthly limit reached. Upgrade to Pro.' });
|
||||
}
|
||||
const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0);
|
||||
|
||||
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;
|
||||
@ -1715,29 +1724,16 @@ app.post(
|
||||
|
||||
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]
|
||||
);
|
||||
}
|
||||
}
|
||||
await db.run(
|
||||
`UPDATE user_profile SET resume_optimizations_used = resume_optimizations_used + 1 WHERE user_id = ?`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
// Calculate remaining optimizations
|
||||
const remainingOptimizations = totalLimit - (userProfile.resume_optimizations_used + 1);
|
||||
|
||||
fs.unlinkSync(filePath);
|
||||
res.json({ optimizedResume });
|
||||
res.json({ optimizedResume, remainingOptimizations });
|
||||
} catch (err) {
|
||||
console.error('Error optimizing resume:', err);
|
||||
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)
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
// If you still want file-saver + docx, import them here
|
||||
|
||||
function ResumeRewrite() {
|
||||
const [resumeFile, setResumeFile] = useState(null);
|
||||
@ -8,11 +7,31 @@ function ResumeRewrite() {
|
||||
const [jobDescription, setJobDescription] = useState('');
|
||||
const [optimizedResume, setOptimizedResume] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [remainingOptimizations, setRemainingOptimizations] = useState(null);
|
||||
const [resetDate, setResetDate] = useState(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) {
|
||||
@ -36,101 +55,62 @@ function ResumeRewrite() {
|
||||
|
||||
setOptimizedResume(res.data.optimizedResume || '');
|
||||
setError('');
|
||||
|
||||
// Refresh remaining optimizations after optimizing
|
||||
fetchRemainingOptimizations();
|
||||
} 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>
|
||||
|
||||
{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">
|
||||
{/* 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"
|
||||
<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"
|
||||
<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"
|
||||
<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>
|
||||
)}
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
*/}
|
||||
<pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border">{optimizedResume}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
BIN
uploads/8af6ea0205ed291fb1dcbb24d00902d1
Normal file
BIN
uploads/8af6ea0205ed291fb1dcbb24d00902d1
Normal file
Binary file not shown.
BIN
uploads/9a869a3048387509ac738d88f2b470f4
Normal file
BIN
uploads/9a869a3048387509ac738d88f2b470f4
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