249 lines
7.3 KiB
TypeScript
249 lines
7.3 KiB
TypeScript
import React, { useState, useRef, useEffect } from "react";
|
|
import {
|
|
Box,
|
|
Link,
|
|
Typography,
|
|
Avatar,
|
|
Grid,
|
|
SxProps,
|
|
Tooltip,
|
|
IconButton,
|
|
} from "@mui/material";
|
|
import { Card, CardContent, 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";
|
|
import { DeleteConfirmation } from "../DeleteConfirmation";
|
|
|
|
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 = "", elevation = 1, 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) => {
|
|
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: any) => {
|
|
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) => {
|
|
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) => {
|
|
e.stopPropagation();
|
|
deleteCandidate(candidate.id);
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { CandidateInfo };
|