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 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
|
|
||||||
if (userPlanRow?.is_pro_premium) userPlan = 'pro';
|
|
||||||
else if (userPlanRow?.is_premium) userPlan = 'premium';
|
|
||||||
|
|
||||||
|
|
||||||
if (userPlan === 'premium') {
|
const userProfile = await db.get(
|
||||||
const usageRow = await db.get(
|
`SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count
|
||||||
`SELECT usage_count FROM feature_usage
|
FROM user_profile
|
||||||
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
|
WHERE user_id = ?`,
|
||||||
[userId, usageMonth]
|
[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]
|
||||||
);
|
);
|
||||||
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)
|
||||||
|
@ -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>
|
||||||
|
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