iris_backend / src /pages /ApplicantProfile.jsx
Saandraahh's picture
Fix admin dashboard notification loop and add resume refresh notification
fdd5c82
import React, { useState, useEffect } from 'react';
import { supabase } from '../supabaseClient';
import ApplicantLayout from '../components/ApplicantLayout'; // The Navigation Wrapper
import ProfilePage from '../components/ProfilePage'; // The UI Component
import { motion, AnimatePresence } from 'framer-motion';
export default function ApplicantProfile({ onNavigate }) {
// --- 1. STATE VARIABLES ---
const [formData, setFormData] = useState({});
const [originalFormData, setOriginalFormData] = useState(null);
const [loading, setLoading] = useState(true);
const [resumeFile, setResumeFile] = useState(null);
const [avatarFile, setAvatarFile] = useState(null);
const [avatarUrl, setAvatarUrl] = useState(null);
const [photoPreviewUrl, setPhotoPreviewUrl] = useState(null);
const [showFullProfile, setShowFullProfile] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [isExtracting, setIsExtracting] = useState(false);
const [showRefreshNotification, setShowRefreshNotification] = useState(false);
// --- 2. FETCH DATA ---
useEffect(() => {
const fetchInitialData = async () => {
setLoading(true);
try {
// Get current user
const { data: { user } } = await supabase.auth.getUser();
if (user) {
// Fetch Profile using maybeSingle() to avoid errors if empty
const { data: profile, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.maybeSingle();
if (error) {
console.error("Error fetching profile:", error.message);
}
if (profile) {
// Profile exists - Load it
const combinedData = { ...profile, email: user.email };
setFormData(combinedData);
setOriginalFormData(combinedData);
if (profile.avatar_url) {
setAvatarUrl(profile.avatar_url);
}
} else {
// New user - Initialize with just email
setFormData({ email: user.email });
}
}
} catch (error) {
console.error("Unexpected error:", error);
} finally {
// ✅ CRITICAL FIX: This ensures loading ALWAYS stops
setLoading(false);
}
};
fetchInitialData();
}, []);
// --- 3. HANDLERS ---
const handleEditClick = () => {
setFormData(currentData => {
const hasExperience = currentData.work_experience && currentData.work_experience.length > 0;
if (!hasExperience) {
return {
...currentData,
work_experience: [{ id: Date.now(), company: '', role: '', years: '' }]
};
}
return currentData;
});
setShowFullProfile(true);
setIsEditing(true);
};
const handleCancelClick = () => {
if (originalFormData) setFormData(originalFormData);
setAvatarFile(null);
setResumeFile(null);
setPhotoPreviewUrl(null);
setIsEditing(false);
};
const handleProfileChange = (e) => {
const { name, value, type, checked } = e.target;
const newValue = type === 'checkbox' ? checked : value;
setFormData(prev => ({ ...prev, [name]: newValue }));
};
const handleAddExperience = () => {
const newExperience = { id: Date.now(), company: '', role: '', years: '' };
setFormData(prev => ({
...prev,
work_experience: [...(prev.work_experience || []), newExperience]
}));
};
const handleExperienceChange = (index, e) => {
const { name, value } = e.target;
const updatedExperience = [...(formData.work_experience || [])];
updatedExperience[index] = { ...updatedExperience[index], [name]: value };
setFormData(prev => ({ ...prev, work_experience: updatedExperience }));
};
const handleResumeFileChange = (e) => {
if (!isEditing || !e.target.files || e.target.files.length === 0) return;
setResumeFile(e.target.files[0]);
};
const handleAvatarFileChange = (e) => {
if (!isEditing || !e.target.files || e.target.files.length === 0) return;
const file = e.target.files[0];
setAvatarFile(file);
setPhotoPreviewUrl(URL.createObjectURL(file));
};
const handleSaveProfile = async () => {
setIsSaving(true);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
try {
const updates = { ...formData, id: user.id, updated_at: new Date() };
delete updates.email; // Don't try to update email in profiles table
if (avatarFile) {
const filePath = `${user.id}/${Date.now()}_${avatarFile.name}`;
await supabase.storage.from('avatars').upload(filePath, avatarFile, { upsert: true });
const { data: urlData } = supabase.storage.from('avatars').getPublicUrl(filePath);
updates.avatar_url = urlData.publicUrl;
}
if (resumeFile) {
// Delete old resume if it exists to prevent duplication
if (originalFormData?.resume_url) {
try {
const oldPath = originalFormData.resume_url;
const { error: removeError } = await supabase.storage.from('resume').remove([oldPath]);
if (removeError) console.warn("Could not delete old resume:", removeError.message);
} catch (e) {
console.warn("Exception during old resume removal:", e);
}
}
const filePath = `${user.id}/${Date.now()}_${resumeFile.name}`;
// Make sure your bucket is named 'resumes' (plural) or 'resume' (singular) to match your Supabase Storage
await supabase.storage.from('resume').upload(filePath, resumeFile, { upsert: true });
updates.resume_url = filePath;
}
const { error } = await supabase.from('profiles').upsert(updates);
if (error) throw error;
setSaveSuccess(true);
if (updates.avatar_url) setAvatarUrl(updates.avatar_url);
setOriginalFormData(formData);
setPhotoPreviewUrl(null);
setIsEditing(false);
// ✅ Show refresh notification 10 seconds after uploading a new resume
if (resumeFile) {
setTimeout(() => {
setShowRefreshNotification(true);
// Auto-hide after 15 seconds
setTimeout(() => {
setShowRefreshNotification(false);
}, 15000);
}, 10000); // 10 seconds delay
}
} catch (error) {
alert(`Error saving profile: ${error.message}`);
} finally {
setIsSaving(false);
setTimeout(() => setSaveSuccess(false), 3000);
}
};
// --- 4. RENDER ---
return (
<ApplicantLayout activePage="applicant-profile" onNavigate={onNavigate}>
<AnimatePresence>
{showRefreshNotification && (
<motion.div
initial={{ opacity: 0, y: -50, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: -50, x: '-50%' }}
style={{
position: 'fixed',
top: '30px',
left: '50%',
backgroundColor: '#10b981', // Emerald green
color: 'white',
padding: '1rem 1.5rem',
borderRadius: '0.75rem',
boxShadow: '0 10px 25px rgba(0,0,0,0.2)',
zIndex: 9999,
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '1rem',
border: '1px solid rgba(255,255,255,0.2)'
}}
>
<span>✨ We've analyzed your newly uploaded resume! Please refresh the page to view your auto-filled profile fields.</span>
<button
onClick={() => window.location.reload()}
style={{
background: 'white',
color: '#10b981',
border: 'none',
padding: '0.5rem 1rem',
borderRadius: '0.5rem',
cursor: 'pointer',
fontWeight: 'bold',
whiteSpace: 'nowrap',
transition: 'all 0.2s',
outline: 'none'
}}
onMouseEnter={(e) => e.target.style.transform = 'scale(1.05)'}
onMouseLeave={(e) => e.target.style.transform = 'scale(1)'}
>
Refresh Now
</button>
<button
onClick={() => setShowRefreshNotification(false)}
style={{
background: 'transparent',
border: 'none',
color: 'white',
fontSize: '1.2rem',
cursor: 'pointer',
padding: '0',
marginLeft: '0.5rem'
}}
>
×
</button>
</motion.div>
)}
</AnimatePresence>
<ProfilePage
profileData={formData}
loading={loading}
avatarUrl={avatarUrl}
photoPreviewUrl={photoPreviewUrl}
resumeFile={resumeFile}
isSaving={isSaving}
saveSuccess={saveSuccess}
isEditing={isEditing}
isExtracting={isExtracting}
showFullProfile={showFullProfile}
setShowFullProfile={setShowFullProfile}
// Pass Handlers
handleEditClick={handleEditClick}
handleCancelClick={handleCancelClick}
handleProfileChange={handleProfileChange}
handleExperienceChange={handleExperienceChange}
handleAddExperience={handleAddExperience}
handleSaveProfile={handleSaveProfile}
handleFileChange={handleResumeFileChange}
handlePhotoChange={handleAvatarFileChange}
/>
</ApplicantLayout>
);
}