diff --git a/frontend/src/pages/candidate/Dashboard.tsx b/frontend/src/pages/candidate/Dashboard.tsx new file mode 100644 index 0000000..2fe8098 --- /dev/null +++ b/frontend/src/pages/candidate/Dashboard.tsx @@ -0,0 +1,138 @@ +import React, {useState} from 'react'; +import { + Box, + Card, + CardContent, + Typography, + Button, + LinearProgress, + List, + ListItem, + ListItemIcon, + ListItemText, + ListItemButton, + Divider, + Chip, + Stack +} from '@mui/material'; +import { + Dashboard as DashboardIcon, + Person as PersonIcon, + Work as WorkIcon, + Article as ArticleIcon, + Description as DescriptionIcon, + Quiz as QuizIcon, + Analytics as AnalyticsIcon, + Settings as SettingsIcon, + Add as AddIcon, + Visibility as VisibilityIcon, + Download as DownloadIcon, + ContactMail as ContactMailIcon, + Edit as EditIcon, + TipsAndUpdates as TipsIcon, + SettingsBackupRestore, + BubbleChart +} from '@mui/icons-material'; +import { useAuth } from 'hooks/AuthContext'; +import { LoadingPage } from 'pages/LoadingPage'; +import { LoginRequired } from 'pages/LoginRequired'; +import { BackstoryPageProps } from 'components/BackstoryTab'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import { BetaPage } from 'pages/BetaPage'; +import { map } from 'lodash'; + +import { CandidateDashboard } from 'pages/candidate/dashboard/Dashboard'; +import { CandidateProfile } from 'pages/candidate/dashboard/Profile'; +import { VectorVisualizer } from 'components/VectorVisualizer'; +import { DocumentManager } from 'components/DocumentManager'; +import { JobPicker } from 'components/ui/JobPicker'; + +interface DashboardProps extends BackstoryPageProps { + userName?: string; + profileCompletion?: number; +} + +const CandidateDashboardPage: React.FC = (props: DashboardProps) => { + const navigate = useNavigate(); + const { subPage = 'dashboard' } = useParams(); + const [activeTab, setActiveTab] = useState(subPage); + const { user, isLoading, isInitializing, isAuthenticated } = useAuth(); + const profileCompletion = 75; + const { setSnack, submitQuery } = props; + const backstoryProps = { setSnack, submitQuery }; + + const sidebarItems = [ + { text: 'Dashboard', icon: ,path: '/', element: }, + { text: 'Profile', icon: ,path: '/profile', element: }, + { text: 'Jobs', icon: ,path: '/jobs', element: }, + { text: 'Resumes', icon: ,path: '/resumes', element: Candidate resumes page }, + { text: 'Content', icon: , path: '/rag', element: }, + { text: 'Q&A Setup', icon: ,path: '/q-a-setup', element: Candidate q&a setup page }, + { text: 'Analytics', icon: ,path: '/analytics', element: Candidate analytics page }, + { text: 'Settings', icon: ,path: '/settings', element: Candidate settings page }, +] + + if (isLoading || isInitializing) { + return (); + } + if (!user || !isAuthenticated) { + return (); + } + if (user.userType !== 'candidate') { + setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning'); + navigate('/'); + return (<>); + } + + return ( + + {/* Sidebar */} + + + {sidebarItems.map((item, index) => ( + + {setActiveTab(item.text.toLowerCase()); navigate(`/candidate/dashboard/${item.text.toLowerCase()}`);}} + > + + {item.icon} + + + + + ))} + + + + {/* Main Content */} + + { sidebarItems.map(item => + {item.element} + )} + + + ); +}; + +export { CandidateDashboardPage }; \ No newline at end of file diff --git a/frontend/src/pages/candidate/dashboard/Dashboard.tsx b/frontend/src/pages/candidate/dashboard/Dashboard.tsx new file mode 100644 index 0000000..e14cb79 --- /dev/null +++ b/frontend/src/pages/candidate/dashboard/Dashboard.tsx @@ -0,0 +1,210 @@ +import React from 'react'; +import { + Box, + Card, + CardContent, + Typography, + Button, + LinearProgress, + Stack +} from '@mui/material'; +import { + Add as AddIcon, + Visibility as VisibilityIcon, + Download as DownloadIcon, + ContactMail as ContactMailIcon, + Edit as EditIcon, + TipsAndUpdates as TipsIcon, +} from '@mui/icons-material'; +import { useAuth } from 'hooks/AuthContext'; +import { LoginRequired } from 'pages/LoginRequired'; +import { BackstoryElementProps } from 'components/BackstoryTab'; +import { useNavigate } from 'react-router-dom'; +import { ComingSoon } from 'components/ui/ComingSoon'; + +interface CandidateDashboardProps extends BackstoryElementProps { +}; + +const CandidateDashboard = (props: CandidateDashboardProps) => { + const { setSnack, submitQuery } = props; + const backstoryProps = { setSnack, submitQuery }; + const navigate = useNavigate(); + const { user } = useAuth(); + const profileCompletion = 75; + + if (!user) { + return ; + } + + if (user?.userType !== 'candidate') { + setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning'); + navigate('/'); + return (<>); + } + + return (<> + {/* Main Content */} + + + {/* Welcome Section */} + + + Welcome back, {user.firstName}! + + + + + Your profile is {profileCompletion}% complete + + + + + + + + {/* Cards Grid */} + + {/* Top Row */} + + {/* Resume Builder Card */} + + + + Resume Builder + + + + 3 custom resumes + + + + Last created: May 15, 2025 + + + + + + + {/* Recent Activity Card */} + + + + Recent Activity + + + + + + 5 profile views + + + + + 2 resume downloads + + + + + 1 direct contact + + + + + + + + + {/* Bottom Row */} + + {/* Complete Your Backstory Card */} + + + + Complete Your Backstory + + + + + • Add projects + + + • Detail skills + + + • Work history + + + + + + + + {/* Improvement Suggestions Card */} + + + + Improvement Suggestions + + + + + • Add certifications + + + • Enhance your project details + + + + + + + + + + + + ); +}; + +export { CandidateDashboard }; \ No newline at end of file diff --git a/frontend/src/pages/candidate/dashboard/Profile.tsx b/frontend/src/pages/candidate/dashboard/Profile.tsx new file mode 100644 index 0000000..db24893 --- /dev/null +++ b/frontend/src/pages/candidate/dashboard/Profile.tsx @@ -0,0 +1,954 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Button, + Container, + Grid, + Paper, + TextField, + Typography, + Avatar, + IconButton, + Tabs, + Tab, + useMediaQuery, + CircularProgress, + Snackbar, + Alert, + Card, + CardContent, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + MenuItem, + Select, + FormControl, + InputLabel, + Switch, + FormControlLabel +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { + PhotoCamera, + Edit, + Save, + Cancel, + Add, + Delete, + Work, + School, + EmojiEvents, + LocationOn, + Phone, + Email, + AccountCircle, +} from '@mui/icons-material'; +import { useTheme } from '@mui/material/styles'; +import { useAuth } from "hooks/AuthContext"; +import * as Types from 'types/types'; +import { ComingSoon } from 'components/ui/ComingSoon'; +import { BackstoryPageProps } from 'components/BackstoryTab'; + +// Styled components +const VisuallyHiddenInput = styled('input')({ + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + height: 1, + overflow: 'hidden', + position: 'absolute', + bottom: 0, + left: 0, + whiteSpace: 'nowrap', + width: 1, +}); + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +const CandidateProfile: React.FC = (props: BackstoryPageProps) => { + const { setSnack } = props; + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const { user, updateUserData, apiClient } = useAuth(); + + // Check if user is a candidate + const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null; + + // State management + const [tabValue, setTabValue] = useState(0); + const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({}); + const [loading, setLoading] = useState(false); + const [snackbar, setSnackbar] = useState<{ + open: boolean; + message: string; + severity: "success" | "error" | "info" | "warning"; + }>({ + open: false, + message: '', + severity: 'success' + }); + + // Form data state + const [formData, setFormData] = useState>({}); + const [profileImage, setProfileImage] = useState(null); + + // Dialog states + const [skillDialog, setSkillDialog] = useState(false); + const [experienceDialog, setExperienceDialog] = useState(false); + + // New item states + const [newSkill, setNewSkill] = useState>({ + name: '', + category: '', + level: 'beginner', + yearsOfExperience: 0 + }); + const [newExperience, setNewExperience] = useState>({ + companyName: '', + position: '', + startDate: new Date(), + isCurrent: false, + description: '', + skills: [], + location: { city: '', country: '' } + }); + + useEffect(() => { + if (candidate) { + setFormData(candidate); + setProfileImage(candidate.profileImage || null); + } + }, [candidate]); + + if (!candidate) { + return ( + + + Access denied. This page is only available for candidates. + + + ); + } + + // Handle tab change + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + // Handle form input changes + const handleInputChange = (field: string, value: any) => { + setFormData({ + ...formData, + [field]: value, + }); + }; + + // Handle profile image upload + const handleImageUpload = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + if (await apiClient.uploadCandidateProfile(e.target.files[0])) { + candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, ''); + console.log(`Set profile image to: ${candidate.profileImage}`); + updateUserData(candidate); + } + } + }; + + // Toggle edit mode for a section + const toggleEditMode = (section: string) => { + setEditMode({ + ...editMode, + [section]: !editMode[section] + }); + }; + + // Save changes + const handleSave = async (section: string) => { + setLoading(true); + try { + if (candidate.id) { + const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData); + updateUserData(updatedCandidate); + setSnack('Profile updated successfully!'); + toggleEditMode(section); + } + } catch (error) { + setSnackbar({ + open: true, + message: 'Failed to update profile. Please try again.', + severity: 'error' + }); + } finally { + setLoading(false); + } + }; + + // Cancel edit + const handleCancel = (section: string) => { + setFormData(candidate); + toggleEditMode(section); + }; + + // Add new skill + const handleAddSkill = () => { + if (newSkill.name && newSkill.category) { + const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill]; + setFormData({ ...formData, skills: updatedSkills }); + setNewSkill({ name: '', category: '', level: 'beginner', yearsOfExperience: 0 }); + setSkillDialog(false); + } + }; + + // Remove skill + const handleRemoveSkill = (index: number) => { + const updatedSkills = (formData.skills || []).filter((_, i) => i !== index); + setFormData({ ...formData, skills: updatedSkills }); + }; + + // Add new work experience + const handleAddExperience = () => { + if (newExperience.companyName && newExperience.position) { + const updatedExperience = [...(formData.experience || []), newExperience as Types.WorkExperience]; + setFormData({ ...formData, experience: updatedExperience }); + setNewExperience({ + companyName: '', + position: '', + startDate: new Date(), + isCurrent: false, + description: '', + skills: [], + location: { city: '', country: '' } + }); + setExperienceDialog(false); + } + }; + + // Remove work experience + const handleRemoveExperience = (index: number) => { + const updatedExperience = (formData.experience || []).filter((_, i) => i !== index); + setFormData({ ...formData, experience: updatedExperience }); + }; + + // Basic Information Tab + const renderBasicInfo = () => ( + + + + + {!profileImage && } + + {editMode.basic && ( + <> + + + + + + Update profile photo + + + )} + + + + + {editMode.basic ? ( + handleInputChange('firstName', e.target.value)} + variant="outlined" + /> + ) : (<> + First Name + {candidate.firstName} + )} + + + + {editMode.basic ? ( + handleInputChange('lastName', e.target.value)} + variant="outlined" + /> + ) : (<> + Last Name + {candidate.lastName} + )} + + + + {(false && editMode.basic) ? ( + handleInputChange('email', e.target.value)} + variant="outlined" + /> + ) : (<> + + Email + {candidate.email} + + )} + + + + {editMode.basic ? ( + handleInputChange('phone', e.target.value)} + variant="outlined" + /> + ) : (<> + + Phone + {candidate.phone || 'Not provided'} + + )} + + + + {editMode.basic ? ( + handleInputChange('description', e.target.value)} + variant="outlined" + /> + ) : (<> + Professional Summary + {candidate.description || 'No summary provided'} + )} + + + + {false && editMode.basic ? ( + handleInputChange('location', { + ...formData.location, + city: e.target.value + })} + variant="outlined" + placeholder="City, State, Country" + /> + ) : (<> + + Location + {candidate.location?.city || 'Not specified'} {candidate.location?.country || ''} + + )} + + + + + {editMode.basic ? ( + <> + + + + ) : ( + + )} + + + + ); + + // Skills Tab + const renderSkills = () => ( + + + Skills & Expertise + + + + + {(formData.skills || []).map((skill, index) => ( + + + + + + + {skill.name} + + + {skill.category} + + + {skill.yearsOfExperience && ( + + {skill.yearsOfExperience} years experience + + )} + + handleRemoveSkill(index)} + color="error" + sx={{ ml: 1 }} + > + + + + + + + ))} + + + {(!formData.skills || formData.skills.length === 0) && ( + + No skills added yet. Click "Add Skill" to get started. + + )} + + ); + + // Experience Tab + const renderExperience = () => ( + + + Work Experience + + + + {(formData.experience || []).map((exp, index) => ( + + + + + + {exp.position} + + + {exp.companyName} + + + {exp.startDate?.toLocaleDateString()} - {exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()} + + + {exp.description} + + {exp.skills && exp.skills.length > 0 && ( + + {exp.skills.map((skill, skillIndex) => ( + + ))} + + )} + + handleRemoveExperience(index)} + color="error" + size="small" + sx={{ + alignSelf: { xs: 'flex-end', sm: 'flex-start' }, + ml: { sm: 1 } + }} + > + + + + + + ))} + + {(!formData.experience || formData.experience.length === 0) && ( + + No work experience added yet. Click "Add Experience" to get started. + + )} + + ); + + + const renderEducation = () => ( + + + Education + + + + {(!formData.experience || formData.experience.length === 0) && ( + + No work experience added yet. Click "Add Experience" to get started. + + )} + + ); + + return ( + + + + + } + iconPosition={isMobile ? "top" : "start"} + /> + } + iconPosition={isMobile ? "top" : "start"} + /> + } + iconPosition={isMobile ? "top" : "start"} + /> + } + iconPosition={isMobile ? "top" : "start"} + /> + + + + + {renderBasicInfo()} + + + + {renderSkills()} + + + + {renderExperience()} + + + + {renderEducation()} + + + + {/* Add Skill Dialog */} + setSkillDialog(false)} + maxWidth="sm" + fullWidth + fullScreen={isMobile} + PaperProps={{ + sx: { + ...(isMobile && { + margin: 0, + width: '100%', + height: '100%', + maxHeight: '100%' + }) + } + }} + > + Add New Skill + + + + setNewSkill({ ...newSkill, name: e.target.value })} + size={isMobile ? "small" : "medium"} + /> + + + setNewSkill({ ...newSkill, category: e.target.value })} + placeholder="e.g., Programming, Design, Marketing" + size={isMobile ? "small" : "medium"} + /> + + + + Proficiency Level + + + + + setNewSkill({ ...newSkill, yearsOfExperience: parseInt(e.target.value) || 0 })} + size={isMobile ? "small" : "medium"} + /> + + + + + + + + + + {/* Add Experience Dialog */} + setExperienceDialog(false)} + maxWidth="md" + fullWidth + fullScreen={isMobile} + PaperProps={{ + sx: { + ...(isMobile && { + margin: 0, + width: '100%', + height: '100%', + maxHeight: '100%' + }) + } + }} + > + Add Work Experience + + + + setNewExperience({ ...newExperience, companyName: e.target.value })} + size={isMobile ? "small" : "medium"} + /> + + + setNewExperience({ ...newExperience, position: e.target.value })} + size={isMobile ? "small" : "medium"} + /> + + + setNewExperience({ ...newExperience, startDate: new Date(e.target.value) })} + InputLabelProps={{ shrink: true }} + size={isMobile ? "small" : "medium"} + /> + + + setNewExperience({ ...newExperience, isCurrent: e.target.checked })} + size={isMobile ? "small" : "medium"} + /> + } + label="Currently working here" + sx={{ + '& .MuiFormControlLabel-label': { + fontSize: { xs: '0.875rem', sm: '1rem' } + } + }} + /> + + + setNewExperience({ ...newExperience, description: e.target.value })} + placeholder="Describe your responsibilities and achievements..." + size={isMobile ? "small" : "medium"} + /> + + + + + + + + + + {/* Snackbar for notifications */} + setSnackbar({ ...snackbar, open: false })} + > + setSnackbar({ ...snackbar, open: false })} + severity={snackbar.severity} + sx={{ width: '100%' }} + > + {snackbar.message} + + + + ); +}; + +export { CandidateProfile }; \ No newline at end of file