227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Box, Link, Typography, Avatar, SxProps, Tooltip, IconButton } from '@mui/material';
|
|
import { Divider, useTheme } from '@mui/material';
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
import { useMediaQuery } from '@mui/material';
|
|
import { Candidate, CandidateAI } from 'types/types';
|
|
import { CopyBubble } from 'components/CopyBubble';
|
|
import { rest } from 'lodash';
|
|
import { AIBanner } from 'components/ui/AIBanner';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
|
|
interface CandidateInfoProps {
|
|
candidate: Candidate;
|
|
sx?: SxProps;
|
|
action?: string;
|
|
elevation?: number;
|
|
variant?: 'minimal' | 'small' | 'normal' | undefined;
|
|
}
|
|
|
|
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
|
|
const { candidate } = props;
|
|
const { user, apiClient } = useAuth();
|
|
const { sx, action = '', variant = 'normal' } = props;
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
|
|
const ai: CandidateAI | null = 'isAI' in candidate ? (candidate as CandidateAI) : null;
|
|
const isAdmin = user?.isAdmin;
|
|
|
|
// State for description expansion
|
|
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
|
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
|
const descriptionRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Check if description needs truncation
|
|
useEffect(() => {
|
|
if (descriptionRef.current && candidate.description) {
|
|
const element = descriptionRef.current;
|
|
// Check if the scrollHeight is greater than clientHeight (meaning content is truncated)
|
|
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
|
|
}
|
|
}, [candidate.description]);
|
|
|
|
const deleteCandidate = async (candidateId: string | undefined): Promise<void> => {
|
|
if (candidateId) {
|
|
await apiClient.deleteCandidate(candidateId);
|
|
}
|
|
};
|
|
|
|
if (!candidate) {
|
|
return <Box>No user loaded.</Box>;
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
transition: 'all 0.3s ease',
|
|
flexGrow: 1,
|
|
p: isMobile ? 1 : 2,
|
|
height: '100%',
|
|
flexDirection: 'column',
|
|
alignItems: 'stretch',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
...sx,
|
|
}}
|
|
{...rest}
|
|
>
|
|
{ai && <AIBanner variant={variant} />}
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
|
<Avatar
|
|
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
|
alt={`${candidate.fullName}'s profile`}
|
|
sx={{
|
|
alignSelf: 'flex-start',
|
|
width: isMobile ? 40 : 80,
|
|
height: isMobile ? 40 : 80,
|
|
border: '2px solid #e0e0e0',
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ ml: 1 }}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
mb: 1,
|
|
}}
|
|
>
|
|
<Box>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: isMobile ? 'column' : 'row',
|
|
alignItems: 'left',
|
|
gap: 1,
|
|
'& > .MuiTypography-root': { m: 0 },
|
|
}}
|
|
>
|
|
{action !== '' && <Typography variant="body1">{action}</Typography>}
|
|
{action === '' && (
|
|
<Typography
|
|
variant="h5"
|
|
component="h1"
|
|
sx={{
|
|
fontWeight: 'bold',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{candidate.fullName}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
<Box sx={{ fontSize: '0.75rem', alignItems: 'center' }}>
|
|
<Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link>
|
|
<CopyBubble
|
|
onClick={(event): void => {
|
|
event.stopPropagation();
|
|
}}
|
|
tooltip="Copy link"
|
|
content={`${window.location.origin}/u/{candidate.username}`}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box>
|
|
{!isMobile && variant === 'normal' && (
|
|
<Box sx={{ minHeight: '5rem' }}>
|
|
<Typography
|
|
ref={descriptionRef}
|
|
variant="body1"
|
|
color="text.secondary"
|
|
sx={{
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
lineHeight: 1.5,
|
|
fontSize: '0.8rem !important',
|
|
}}
|
|
>
|
|
{candidate.description}
|
|
</Typography>
|
|
{shouldShowMoreButton && (
|
|
<Link
|
|
component="button"
|
|
variant="body2"
|
|
onClick={(e): void => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDescriptionExpanded(!isDescriptionExpanded);
|
|
}}
|
|
sx={{
|
|
color: theme.palette.primary.main,
|
|
textDecoration: 'none',
|
|
cursor: 'pointer',
|
|
fontSize: '0.725rem',
|
|
fontWeight: 500,
|
|
mt: 0.5,
|
|
display: 'block',
|
|
'&:hover': {
|
|
textDecoration: 'underline',
|
|
},
|
|
}}
|
|
>
|
|
[{isDescriptionExpanded ? 'less' : 'more'}]
|
|
</Link>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{variant !== 'small' && variant !== 'minimal' && (
|
|
<>
|
|
<Divider sx={{ my: 2 }} />
|
|
|
|
{candidate.location && (
|
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
<strong>Location:</strong> {candidate.location.city},{' '}
|
|
{candidate.location.state || candidate.location.country}
|
|
</Typography>
|
|
)}
|
|
{candidate.email && (
|
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
<strong>Email:</strong> {candidate.email}
|
|
</Typography>
|
|
)}
|
|
{candidate.phone && (
|
|
<Typography variant="body2">
|
|
<strong>Phone:</strong> {candidate.phone}
|
|
</Typography>
|
|
)}
|
|
</>
|
|
)}
|
|
</Box>
|
|
{isAdmin && ai && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-start',
|
|
}}
|
|
>
|
|
<Tooltip title="Delete Job">
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e): void => {
|
|
e.stopPropagation();
|
|
deleteCandidate(candidate.id);
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { CandidateInfo };
|