backstory/frontend/src/components/ui/CandidateInfo.tsx

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