feat(workspace): refine career workflows

Make job/CV comparisons language-aware and filter recruitment noise. Improve responsive career navigation, shared spacing, dashboard priorities, settings, localized workspace controls, and portable browser tests.
This commit is contained in:
cesnimda
2026-08-29 16:44:44 +02:00
parent 4d4af47651
commit 9edcbfc5de
29 changed files with 827 additions and 236 deletions
@@ -30,12 +30,13 @@ function isActive(run: CareerWorkspaceImportRun) {
: run.status === "queued" || run.status === "running";
}
function ActionCard({ title, body, href, label, icon }: {
function ActionCard({ title, body, href, label, icon, onNavigate }: {
title: string;
body: string;
href: string;
label: string;
icon: React.ReactNode;
onNavigate?: () => void;
}) {
return (
<Paper variant="outlined" component="article" sx={{ p: 2, borderRadius: 3, display: "flex", flexDirection: "column", gap: 1.25 }}>
@@ -44,16 +45,24 @@ function ActionCard({ title, body, href, label, icon }: {
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{title}</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ flex: 1 }}>{body}</Typography>
<Button href={href} variant="text" sx={{ alignSelf: "flex-start", px: 0.5 }}>{label}</Button>
<Button
href={href}
onClick={onNavigate ? (event) => { event.preventDefault(); onNavigate(); } : undefined}
variant="text"
sx={{ alignSelf: "flex-start", px: 0.5 }}
>
{label}
</Button>
</Paper>
);
}
export default function CareerWorkspaceOverview({ completeness, runs, loading, loadError }: {
export default function CareerWorkspaceOverview({ completeness, runs, loading, loadError, onNavigate }: {
completeness: CareerWorkspaceCompleteness | null;
runs: CareerWorkspaceImportRun[];
loading: boolean;
loadError: string | null;
onNavigate?: (section: "profile" | "import") => void;
}) {
const { language, t } = useI18n();
const [recentCvs, setRecentCvs] = useState<CvVariantSummary[]>([]);
@@ -93,11 +102,11 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;
return (
<Box component="section" aria-labelledby="career-workspace-title" sx={{ display: "grid", gap: 2 }}>
<Box component="section" aria-labelledby="career-workspace-overview-title" sx={{ display: "grid", gap: 2 }}>
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 4 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }}>
<Box>
<Typography id="career-workspace-title" component="h1" variant="h5" sx={{ fontWeight: 900 }}>{t("careerOverviewTitle")}</Typography>
<Typography id="career-workspace-overview-title" component="h2" variant="h5" sx={{ fontWeight: 900 }}>{t("careerWorkspaceOverviewTab")}</Typography>
<Typography color="text.secondary">{t("careerOverviewSubtitle")}</Typography>
</Box>
<Button variant="contained" startIcon={<DescriptionOutlinedIcon />} href="/career/builder">{t("careerOverviewOpenBuilder")}</Button>
@@ -116,20 +125,20 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
{!loading && completeness?.missing.length ? (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).join(", ") })}</Typography>
) : null}
<Button href="#career-profile-editor" size="small" sx={{ mt: 1, px: 0.5 }}>{t("careerOverviewImproveProfile")}</Button>
<Button href="/career?section=profile" onClick={onNavigate ? (event) => { event.preventDefault(); onNavigate("profile"); } : undefined} size="small" sx={{ mt: 1, px: 0.5 }}>{t("careerOverviewImproveProfile")}</Button>
</Box>
<Alert severity={importState.severity} sx={{ borderRadius: 3, alignItems: "flex-start" }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("careerOverviewCvImport")}</Typography>
<Typography variant="body2">{importState.message}</Typography>
<Button href="#career-cv-import" color="inherit" size="small" sx={{ mt: 0.75, px: 0.5 }}>{importState.label}</Button>
<Button href="/career?section=import" onClick={onNavigate ? (event) => { event.preventDefault(); onNavigate("import"); } : undefined} color="inherit" size="small" sx={{ mt: 0.75, px: 0.5 }}>{importState.label}</Button>
</Alert>
</Box>
</Paper>
<Box component="nav" aria-label={t("careerOverviewActions")} sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(2, minmax(0, 1fr))", xl: "repeat(4, minmax(0, 1fr))" }, gap: 1.5 }}>
<ActionCard title={t("careerOverviewCareerProfile")} body={t("careerOverviewCareerProfileBody")} href="#career-profile-editor" label={t("careerOverviewEditProfile")} icon={<PersonOutlineIcon />} />
<ActionCard title={t("careerOverviewImportCv")} body={t("careerOverviewImportBody")} href="#career-cv-import" label={importState.label} icon={<UploadFileOutlinedIcon />} />
<ActionCard title={t("careerOverviewCareerProfile")} body={t("careerOverviewCareerProfileBody")} href="/career?section=profile" onNavigate={onNavigate ? () => onNavigate("profile") : undefined} label={t("careerOverviewEditProfile")} icon={<PersonOutlineIcon />} />
<ActionCard title={t("careerOverviewImportCv")} body={t("careerOverviewImportBody")} href="/career?section=import" onNavigate={onNavigate ? () => onNavigate("import") : undefined} label={importState.label} icon={<UploadFileOutlinedIcon />} />
<ActionCard title={t("careerOverviewGeneralCv")} body={t("careerOverviewGeneralCvBody")} href="/career/builder" label={t("careerOverviewCreateGeneralCv")} icon={<DescriptionOutlinedIcon />} />
<ActionCard title={t("careerOverviewJobCv")} body={t("careerOverviewJobCvBody")} href="/jobs" label={t("careerOverviewChooseJob")} icon={<WorkOutlineIcon />} />
</Box>