Added missing files
This commit is contained in:
parent
588b1d9b61
commit
18863a23d9
138
frontend/src/pages/candidate/Dashboard.tsx
Normal file
138
frontend/src/pages/candidate/Dashboard.tsx
Normal file
@ -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<DashboardProps> = (props: DashboardProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { subPage = 'dashboard' } = useParams();
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(subPage);
|
||||||
|
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
||||||
|
const profileCompletion = 75;
|
||||||
|
const { setSnack, submitQuery } = props;
|
||||||
|
const backstoryProps = { setSnack, submitQuery };
|
||||||
|
|
||||||
|
const sidebarItems = [
|
||||||
|
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
|
||||||
|
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
|
||||||
|
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
|
||||||
|
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
|
||||||
|
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{display: "flex", width: "100%", flexDirection: "column"}}><VectorVisualizer {...backstoryProps} /><DocumentManager {...backstoryProps} /></Box>},
|
||||||
|
{ text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> },
|
||||||
|
{ text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> },
|
||||||
|
{ text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isLoading || isInitializing) {
|
||||||
|
return (<LoadingPage {...props}/>);
|
||||||
|
}
|
||||||
|
if (!user || !isAuthenticated) {
|
||||||
|
return (<LoginRequired {...props}/>);
|
||||||
|
}
|
||||||
|
if (user.userType !== 'candidate') {
|
||||||
|
setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning');
|
||||||
|
navigate('/');
|
||||||
|
return (<></>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 250,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRight: '1px solid #e0e0e0',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{sidebarItems.map((item, index) => (
|
||||||
|
<ListItem key={index} disablePadding sx={{ mb: 0.5 }}>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (item.text.toLowerCase() === activeTab )? '#e3f2fd' : 'transparent',
|
||||||
|
color: (item.text.toLowerCase() === activeTab ) ? '#1976d2' : '#666',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: (item.text.toLowerCase() === activeTab ) ? '#e3f2fd' : '#f5f5f5',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={()=>{setActiveTab(item.text.toLowerCase()); navigate(`/candidate/dashboard/${item.text.toLowerCase()}`);}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ color: 'inherit', minWidth: 40 }}>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={item.text}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
fontWeight: (item.text === activeTab ) ? 600 : 400,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<Box sx={{ flex: 1, p: 3 }}>
|
||||||
|
{ sidebarItems.map(item =>
|
||||||
|
<Box sx={{display: (item.text.toLowerCase() === activeTab) ? "flex": "none", width: "100%"}}>{item.element}</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CandidateDashboardPage };
|
210
frontend/src/pages/candidate/dashboard/Dashboard.tsx
Normal file
210
frontend/src/pages/candidate/dashboard/Dashboard.tsx
Normal file
@ -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 <LoginRequired {...backstoryProps}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */}
|
||||||
|
<ComingSoon>
|
||||||
|
<Box sx={{ flex: 1, p: 3 }}>
|
||||||
|
{/* Welcome Section */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h4" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||||
|
Welcome back, {user.firstName}!
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
||||||
|
Your profile is {profileCompletion}% complete
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={profileCompletion}
|
||||||
|
sx={{
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: '#e0e0e0',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
backgroundColor: '#4caf50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
onClick={(e) => {e.stopPropagation(); navigate('/candidate/dashboard/profile'); }}
|
||||||
|
>
|
||||||
|
Complete Your Profile
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Cards Grid */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{/* Top Row */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
|
{/* Resume Builder Card */}
|
||||||
|
<Card sx={{ flex: 1, minHeight: 200 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||||
|
Resume Builder
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ mb: 1, color: '#666' }}>
|
||||||
|
3 custom resumes
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ mb: 3, color: '#666' }}>
|
||||||
|
Last created: May 15, 2025
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Activity Card */}
|
||||||
|
<Card sx={{ flex: 1, minHeight: 200 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||||
|
Recent Activity
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<VisibilityIcon sx={{ fontSize: 16, color: '#666' }} />
|
||||||
|
<Typography variant="body2">5 profile views</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<DownloadIcon sx={{ fontSize: 16, color: '#666' }} />
|
||||||
|
<Typography variant="body2">2 resume downloads</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<ContactMailIcon sx={{ fontSize: 16, color: '#666' }} />
|
||||||
|
<Typography variant="body2">1 direct contact</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
View All Activity
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Bottom Row */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
|
{/* Complete Your Backstory Card */}
|
||||||
|
<Card sx={{ flex: 1, minHeight: 200 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||||
|
Complete Your Backstory
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
• Add projects
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
• Detail skills
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
• Work history
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<EditIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Edit Backstory
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Improvement Suggestions Card */}
|
||||||
|
<Card sx={{ flex: 1, minHeight: 200 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
|
||||||
|
Improvement Suggestions
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={1} sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
• Add certifications
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
• Enhance your project details
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<TipsIcon />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
View All Tips
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ComingSoon>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CandidateDashboard };
|
954
frontend/src/pages/candidate/dashboard/Profile.tsx
Normal file
954
frontend/src/pages/candidate/dashboard/Profile.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`profile-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`profile-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && (
|
||||||
|
<Box sx={{
|
||||||
|
p: { xs: 1, sm: 3 },
|
||||||
|
maxWidth: '100%',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CandidateProfile: React.FC<BackstoryPageProps> = (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<Partial<Types.Candidate>>({});
|
||||||
|
const [profileImage, setProfileImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Dialog states
|
||||||
|
const [skillDialog, setSkillDialog] = useState(false);
|
||||||
|
const [experienceDialog, setExperienceDialog] = useState(false);
|
||||||
|
|
||||||
|
// New item states
|
||||||
|
const [newSkill, setNewSkill] = useState<Partial<Types.Skill>>({
|
||||||
|
name: '',
|
||||||
|
category: '',
|
||||||
|
level: 'beginner',
|
||||||
|
yearsOfExperience: 0
|
||||||
|
});
|
||||||
|
const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({
|
||||||
|
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 (
|
||||||
|
<Container maxWidth="md" sx={{ mt: 4 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
Access denied. This page is only available for candidates.
|
||||||
|
</Alert>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<HTMLInputElement>) => {
|
||||||
|
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 = () => (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", "& .entry": { flexDirection: "column", fontSize: "0.9rem", display: "flex", mt: 1 }, "& .title": { display: "flex", fontWeight: "bold" } }}>
|
||||||
|
<Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
src={profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
||||||
|
sx={{
|
||||||
|
width: { xs: 80, sm: 120 },
|
||||||
|
height: { xs: 80, sm: 120 },
|
||||||
|
mb: { xs: 1, sm: 2 },
|
||||||
|
border: `2px solid ${theme.palette.primary.main}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!profileImage && <AccountCircle sx={{ fontSize: { xs: 50, sm: 80 } }} />}
|
||||||
|
</Avatar>
|
||||||
|
{editMode.basic && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
aria-label="upload picture"
|
||||||
|
component="label"
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
<PhotoCamera />
|
||||||
|
<VisuallyHiddenInput
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageUpload}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="caption" color="textSecondary" sx={{ textAlign: 'center', fontSize: { xs: '0.7rem', sm: '0.75rem' } }}>
|
||||||
|
Update profile photo
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{editMode.basic ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="First Name"
|
||||||
|
value={formData.firstName || ''}
|
||||||
|
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (<>
|
||||||
|
<Box className="title">First Name</Box>
|
||||||
|
<Box className="value">{candidate.firstName}</Box>
|
||||||
|
</>)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{editMode.basic ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Last Name"
|
||||||
|
value={formData.lastName || ''}
|
||||||
|
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (<>
|
||||||
|
<Box className="title">Last Name</Box>
|
||||||
|
<Box className="value">{candidate.lastName}</Box>
|
||||||
|
</>)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{(false && editMode.basic) ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email || ''}
|
||||||
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (<>
|
||||||
|
<Box className="title"><Email sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Email</Box>
|
||||||
|
<Box className="value">{candidate.email}</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{editMode.basic ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Phone"
|
||||||
|
value={formData.phone || ''}
|
||||||
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (<>
|
||||||
|
<Box className="title"><Phone sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Phone</Box>
|
||||||
|
<Box className="value">{candidate.phone || 'Not provided'}</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{editMode.basic ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
label="Professional Summary"
|
||||||
|
value={formData.description || ''}
|
||||||
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
) : (<>
|
||||||
|
<Box className="title">Professional Summary</Box>
|
||||||
|
<Box className="value">{candidate.description || 'No summary provided'}</Box>
|
||||||
|
</>)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
{false && editMode.basic ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Location"
|
||||||
|
value={formData.location?.city || ''}
|
||||||
|
onChange={(e) => handleInputChange('location', {
|
||||||
|
...formData.location,
|
||||||
|
city: e.target.value
|
||||||
|
})}
|
||||||
|
variant="outlined"
|
||||||
|
placeholder="City, State, Country"
|
||||||
|
/>
|
||||||
|
) : (<><Box className="title">
|
||||||
|
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Location</Box>
|
||||||
|
<Box className="value">{candidate.location?.city || 'Not specified'} {candidate.location?.country || ''}</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="entry">
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 2,
|
||||||
|
mt: { xs: 2, sm: 0 }
|
||||||
|
}}>
|
||||||
|
{editMode.basic ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => handleCancel('basic')}
|
||||||
|
startIcon={<Cancel />}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => handleSave('basic')}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => toggleEditMode('basic')}
|
||||||
|
startIcon={<Edit />}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
>
|
||||||
|
Edit Info
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box >
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skills Tab
|
||||||
|
const renderSkills = () => (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: { xs: 'stretch', sm: 'center' },
|
||||||
|
mb: { xs: 2, sm: 3 },
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Typography variant={isMobile ? "subtitle1" : "h6"}>Skills & Expertise</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={() => setSkillDialog(true)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Add Skill
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={{ xs: 1, sm: 2 }} sx={{ maxWidth: '100%' }}>
|
||||||
|
{(formData.skills || []).map((skill, index) => (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||||
|
<Card variant="outlined" sx={{ height: '100%' }}>
|
||||||
|
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography variant={isMobile ? "subtitle2" : "h6"} component="div" sx={{
|
||||||
|
fontSize: { xs: '0.9rem', sm: '1.25rem' },
|
||||||
|
wordBreak: 'break-word'
|
||||||
|
}}>
|
||||||
|
{skill.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontSize: { xs: '0.75rem', sm: '0.875rem' }
|
||||||
|
}}>
|
||||||
|
{skill.category}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={skill.level}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
fontSize: { xs: '0.65rem', sm: '0.75rem' },
|
||||||
|
height: { xs: 20, sm: 24 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{skill.yearsOfExperience && (
|
||||||
|
<Typography variant="caption" display="block" sx={{ fontSize: { xs: '0.65rem', sm: '0.75rem' } }}>
|
||||||
|
{skill.yearsOfExperience} years experience
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleRemoveSkill(index)}
|
||||||
|
color="error"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
>
|
||||||
|
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{(!formData.skills || formData.skills.length === 0) && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
No skills added yet. Click "Add Skill" to get started.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Experience Tab
|
||||||
|
const renderExperience = () => (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: { xs: 'stretch', sm: 'center' },
|
||||||
|
mb: { xs: 2, sm: 3 },
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Typography variant={isMobile ? "subtitle1" : "h6"}>Work Experience</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={() => setExperienceDialog(true)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Add Experience
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{(formData.experience || []).map((exp, index) => (
|
||||||
|
<Card key={index} sx={{ mb: { xs: 1.5, sm: 2 }, overflow: 'hidden' }}>
|
||||||
|
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography variant={isMobile ? "subtitle1" : "h6"} component="div" sx={{
|
||||||
|
fontSize: { xs: '1rem', sm: '1.25rem' },
|
||||||
|
wordBreak: 'break-word'
|
||||||
|
}}>
|
||||||
|
{exp.position}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" color="primary" sx={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontSize: { xs: '0.9rem', sm: '1rem' }
|
||||||
|
}}>
|
||||||
|
{exp.companyName}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8rem', sm: '0.875rem' } }}>
|
||||||
|
{exp.startDate?.toLocaleDateString()} - {exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
mt: 1,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontSize: { xs: '0.8rem', sm: '0.875rem' }
|
||||||
|
}}>
|
||||||
|
{exp.description}
|
||||||
|
</Typography>
|
||||||
|
{exp.skills && exp.skills.length > 0 && (
|
||||||
|
<Box sx={{ mt: { xs: 1, sm: 2 } }}>
|
||||||
|
{exp.skills.map((skill, skillIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={skillIndex}
|
||||||
|
label={skill}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
mr: 0.5,
|
||||||
|
mb: 0.5,
|
||||||
|
fontSize: { xs: '0.65rem', sm: '0.75rem' },
|
||||||
|
height: { xs: 20, sm: 24 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handleRemoveExperience(index)}
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
alignSelf: { xs: 'flex-end', sm: 'flex-start' },
|
||||||
|
ml: { sm: 1 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{(!formData.experience || formData.experience.length === 0) && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
py: { xs: 2, sm: 4 },
|
||||||
|
fontSize: { xs: '0.8rem', sm: '0.875rem' }
|
||||||
|
}}>
|
||||||
|
No work experience added yet. Click "Add Experience" to get started.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const renderEducation = () => (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: { xs: 'stretch', sm: 'center' },
|
||||||
|
mb: { xs: 2, sm: 3 },
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Typography variant={isMobile ? "subtitle1" : "h6"}>Education</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Add />}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Add Education
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{(!formData.experience || formData.experience.length === 0) && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
py: { xs: 2, sm: 4 },
|
||||||
|
fontSize: { xs: '0.8rem', sm: '0.875rem' }
|
||||||
|
}}>
|
||||||
|
No work experience added yet. Click "Add Experience" to get started.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{
|
||||||
|
mt: { xs: 1, sm: 4 },
|
||||||
|
mb: { xs: 1, sm: 4 },
|
||||||
|
px: { xs: 0.5, sm: 3 }
|
||||||
|
}}>
|
||||||
|
<Paper elevation={3} sx={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
mx: { xs: 0, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
|
<Tabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
allowScrollButtonsMobile
|
||||||
|
sx={{
|
||||||
|
'& .MuiTabs-flexContainer': {
|
||||||
|
justifyContent: isMobile ? 'flex-start' : 'center'
|
||||||
|
},
|
||||||
|
'& .MuiTab-root': {
|
||||||
|
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
||||||
|
minWidth: { xs: 60, sm: 120 },
|
||||||
|
padding: { xs: '6px 8px', sm: '12px 16px' }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
label={isMobile ? "Info" : "Basic Info"}
|
||||||
|
icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
|
iconPosition={isMobile ? "top" : "start"}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Skills"
|
||||||
|
icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
|
iconPosition={isMobile ? "top" : "start"}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label={isMobile ? "Work" : "Experience"}
|
||||||
|
icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
|
iconPosition={isMobile ? "top" : "start"}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label={isMobile ? "Edu" : "Education"}
|
||||||
|
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||||
|
iconPosition={isMobile ? "top" : "start"}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={0}>
|
||||||
|
{renderBasicInfo()}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={1}>
|
||||||
|
<ComingSoon>{renderSkills()}</ComingSoon>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={2}>
|
||||||
|
<ComingSoon>{renderExperience()}</ComingSoon>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={3}>
|
||||||
|
<ComingSoon>{renderEducation()}</ComingSoon>
|
||||||
|
</TabPanel>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Add Skill Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={skillDialog}
|
||||||
|
onClose={() => setSkillDialog(false)}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
fullScreen={isMobile}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
...(isMobile && {
|
||||||
|
margin: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add New Skill</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
pt: { xs: 1, sm: 2 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Skill Name"
|
||||||
|
value={newSkill.name || ''}
|
||||||
|
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Category"
|
||||||
|
value={newSkill.category || ''}
|
||||||
|
onChange={(e) => setNewSkill({ ...newSkill, category: e.target.value })}
|
||||||
|
placeholder="e.g., Programming, Design, Marketing"
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<FormControl fullWidth size={isMobile ? "small" : "medium"}>
|
||||||
|
<InputLabel>Proficiency Level</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={newSkill.level || 'beginner'}
|
||||||
|
onChange={(e) => setNewSkill({ ...newSkill, level: e.target.value as Types.SkillLevel })}
|
||||||
|
label="Proficiency Level"
|
||||||
|
>
|
||||||
|
<MenuItem value="beginner">Beginner</MenuItem>
|
||||||
|
<MenuItem value="intermediate">Intermediate</MenuItem>
|
||||||
|
<MenuItem value="advanced">Advanced</MenuItem>
|
||||||
|
<MenuItem value="expert">Expert</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
label="Years of Experience"
|
||||||
|
value={newSkill.yearsOfExperience || 0}
|
||||||
|
onChange={(e) => setNewSkill({ ...newSkill, yearsOfExperience: parseInt(e.target.value) || 0 })}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{
|
||||||
|
p: { xs: 1.5, sm: 3 },
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setSkillDialog(false)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddSkill}
|
||||||
|
variant="contained"
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Add Skill
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Add Experience Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={experienceDialog}
|
||||||
|
onClose={() => setExperienceDialog(false)}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
fullScreen={isMobile}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
...(isMobile && {
|
||||||
|
margin: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '100%'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add Work Experience</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
pt: { xs: 1, sm: 2 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Company Name"
|
||||||
|
value={newExperience.companyName || ''}
|
||||||
|
onChange={(e) => setNewExperience({ ...newExperience, companyName: e.target.value })}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Position/Title"
|
||||||
|
value={newExperience.position || ''}
|
||||||
|
onChange={(e) => setNewExperience({ ...newExperience, position: e.target.value })}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type="date"
|
||||||
|
label="Start Date"
|
||||||
|
value={newExperience.startDate?.toISOString().split('T')[0] || ''}
|
||||||
|
onChange={(e) => setNewExperience({ ...newExperience, startDate: new Date(e.target.value) })}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={newExperience.isCurrent || false}
|
||||||
|
onChange={(e) => setNewExperience({ ...newExperience, isCurrent: e.target.checked })}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Currently working here"
|
||||||
|
sx={{
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: { xs: '0.875rem', sm: '1rem' }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={isMobile ? 3 : 4}
|
||||||
|
label="Job Description"
|
||||||
|
value={newExperience.description || ''}
|
||||||
|
onChange={(e) => setNewExperience({ ...newExperience, description: e.target.value })}
|
||||||
|
placeholder="Describe your responsibilities and achievements..."
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{
|
||||||
|
p: { xs: 1.5, sm: 3 },
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
gap: { xs: 1, sm: 0 }
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setExperienceDialog(false)}
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddExperience}
|
||||||
|
variant="contained"
|
||||||
|
fullWidth={isMobile}
|
||||||
|
size={isMobile ? "small" : "medium"}
|
||||||
|
>
|
||||||
|
Add Experience
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Snackbar for notifications */}
|
||||||
|
<Snackbar
|
||||||
|
open={snackbar.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||||
|
severity={snackbar.severity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{snackbar.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CandidateProfile };
|
Loading…
x
Reference in New Issue
Block a user