Added missing files

This commit is contained in:
James Ketr 2025-06-08 12:53:53 -07:00
parent 588b1d9b61
commit 18863a23d9
3 changed files with 1302 additions and 0 deletions

View 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 };

View 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 };

View 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 };