backstory/frontend/src/components/ui/CandidateInfo.tsx
2025-06-18 14:26:07 -07:00

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