First Commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { Box, Tooltip, Typography, useTheme } from "@mui/material";
|
||||
|
||||
import WorkOutlineIcon from "@mui/icons-material/WorkOutline";
|
||||
import ForumIcon from "@mui/icons-material/Forum";
|
||||
import MarkEmailReadIcon from "@mui/icons-material/MarkEmailRead";
|
||||
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
import { JobApplication } from "../types";
|
||||
|
||||
function normalizeStatus(status?: string): string {
|
||||
if (!status) return "Applied";
|
||||
if (status === "Interviewing") return "Interview";
|
||||
return status;
|
||||
}
|
||||
|
||||
function isTerminalRejected(status: string) {
|
||||
return status === "Rejected" || status === "Ghosted";
|
||||
}
|
||||
|
||||
export default function JobFlowBar({ job }: { job: JobApplication | null }) {
|
||||
const theme = useTheme();
|
||||
const steps = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ key: "applied" as const, label: "Applied", color: theme.palette.info.main, icon: <WorkOutlineIcon fontSize="inherit" /> },
|
||||
{ key: "interview" as const, label: "Interview", color: theme.palette.primary.main, icon: <ForumIcon fontSize="inherit" /> },
|
||||
{ key: "reply" as const, label: "Reply", color: theme.palette.success.main, icon: <MarkEmailReadIcon fontSize="inherit" /> },
|
||||
{ key: "offer" as const, label: "Offer", color: theme.palette.success.dark, icon: <EmojiEventsIcon fontSize="inherit" /> },
|
||||
{ key: "outcome" as const, label: "Outcome", color: theme.palette.text.secondary, icon: <EmojiEventsIcon fontSize="inherit" /> },
|
||||
] as const,
|
||||
[theme.palette.info.main, theme.palette.primary.main, theme.palette.success.main, theme.palette.success.dark, theme.palette.text.secondary],
|
||||
);
|
||||
|
||||
if (!job) return null;
|
||||
|
||||
const status = normalizeStatus(job.status);
|
||||
const reply = Boolean(job.responseReceived);
|
||||
const rejected = isTerminalRejected(status);
|
||||
|
||||
const activeIndex = (() => {
|
||||
if (rejected) return 4;
|
||||
if (status === "Offer") return 3;
|
||||
if (reply) return 2;
|
||||
if (status === "Interview") return 1;
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const breakIndex = rejected ? activeIndex : null;
|
||||
const visibleSteps = breakIndex !== null ? steps.slice(0, breakIndex + 1) : steps;
|
||||
|
||||
const segCount = visibleSteps.length;
|
||||
const segWidth = `${100 / segCount}%`;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="overline" sx={{ display: "block", mb: 0.75 }}>
|
||||
Flow
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: 999,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${alpha(theme.palette.text.primary, 0.12)}`,
|
||||
height: 44,
|
||||
background: alpha("#fff", 0.65),
|
||||
}}
|
||||
>
|
||||
{visibleSteps.map((s, idx) => {
|
||||
const filled = idx <= activeIndex && breakIndex === null;
|
||||
const terminalFilled = breakIndex !== null && idx < breakIndex;
|
||||
const isBreak = breakIndex !== null && idx === breakIndex;
|
||||
|
||||
const bg = filled || terminalFilled ? s.color : alpha(s.color, 0.12);
|
||||
const fg = filled || terminalFilled ? "#fff" : alpha(theme.palette.text.primary, 0.6);
|
||||
|
||||
const title = (() => {
|
||||
if (isBreak) return `${status} (stopped here)`;
|
||||
if (idx === 2 && reply) return "Reply received";
|
||||
return s.label;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Tooltip key={s.key} title={title}>
|
||||
<Box
|
||||
sx={{
|
||||
width: segWidth,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
background: isBreak ? theme.palette.error.main : bg,
|
||||
color: "#fff",
|
||||
borderRight: idx === visibleSteps.length - 1 ? "none" : `1px solid ${alpha("#fff", 0.35)}`,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{isBreak ? (
|
||||
<CloseIcon fontSize="inherit" />
|
||||
) : (
|
||||
<Box sx={{ display: "inline-flex", alignItems: "center", gap: 1, color: fg, fontWeight: 900 }}>
|
||||
<Box sx={{ fontSize: 18, display: "inline-flex" }}>{s.icon}</Box>
|
||||
<span style={{ fontSize: 13 }}>{s.key === "outcome" && rejected ? status : s.label}</span>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isBreak ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: 900,
|
||||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
{status} <span style={{ marginLeft: 8, display: "inline-flex" }}><CloseIcon fontSize="inherit" /></span>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user