Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } |