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
@@ -9,6 +9,7 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
import {
CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi,
} from "../applicationWorkspace";
@@ -20,6 +21,7 @@ import {
// scheduled...). Everything here is the user's to tick, add to, reorder or dismiss.
// docs/architecture/application-workspace.md.
export default function ApplicationChecklist({ jobId, onChanged }: { jobId: number; onChanged?: () => void }) {
const { t } = useI18n();
const [checklist, setChecklist] = useState<Checklist | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
@@ -30,9 +32,9 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
setChecklist(await applicationChecklistApi.get(jobId));
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, "Could not load the checklist."));
setError(getApiErrorMessage(err, t("checklistLoadFailed")));
}
}, [jobId]);
}, [jobId, t]);
useEffect(() => {
load();
@@ -47,11 +49,11 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
await load();
onChanged?.();
} catch (err) {
setError(getApiErrorMessage(err, "Could not update the checklist."));
setError(getApiErrorMessage(err, t("checklistUpdateFailed")));
} finally {
setBusy(false);
}
}, [load, onChanged]);
}, [load, onChanged, t]);
const toggle = (item: ChecklistItem) =>
mutate(() => applicationChecklistApi.update(jobId, item.id, {
@@ -98,15 +100,15 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
{progress && (
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Stack direction="row" alignItems="baseline" justifyContent="space-between" sx={{ mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>Application checklist</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t("checklistTitle")}</Typography>
<Typography variant="body2" color="text.secondary">
{progress.completed} of {progress.total} done
{t("checklistProgress", { completed: progress.completed, total: progress.total })}
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={progress.percent}
aria-label="Checklist completion"
aria-label={t("checklistCompletion")}
sx={{ height: 8, borderRadius: 4 }}
/>
</Paper>
@@ -114,9 +116,11 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
{grouped.map((group) => (
<Paper key={group.key} sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="overline" color="text.secondary">{group.label}</Typography>
<Typography variant="overline" color="text.secondary">{checklistCategoryLabel(t, group.key)}</Typography>
<Stack sx={{ mt: 0.5 }}>
{group.items.map((item) => (
{group.items.map((item) => {
const display = checklistItemDisplay(t, item);
return (
<Stack
key={item.id}
direction="row"
@@ -129,7 +133,7 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
checked={item.status === "done"}
disabled={busy}
onChange={() => toggle(item)}
inputProps={{ "aria-label": item.title }}
inputProps={{ "aria-label": display.title }}
sx={{ mt: -0.25 }}
/>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
@@ -142,40 +146,41 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
color: item.status === "done" ? "text.disabled" : "text.primary",
}}
>
{item.title}
{display.title}
</Typography>
{!item.isSystemGenerated && <Chip size="small" label="Yours" variant="outlined" />}
{item.isAutoCompleted && <Chip size="small" label="Detected" color="success" variant="outlined" />}
{!item.isSystemGenerated && <Chip size="small" label={t("checklistYours")} variant="outlined" />}
{item.isAutoCompleted && <Chip size="small" label={t("checklistDetected")} color="success" variant="outlined" />}
</Stack>
{item.description && (
<Typography variant="caption" color="text.secondary">{item.description}</Typography>
{display.description && (
<Typography variant="caption" color="text.secondary">{display.description}</Typography>
)}
</Box>
<Stack direction="row" className="checklist-actions" sx={{ opacity: { xs: 1, md: 0 }, transition: "opacity .15s" }}>
<Tooltip title="Move up">
<Tooltip title={t("checklistMoveUp")}>
<span>
<IconButton size="small" disabled={busy} aria-label={`Move up: ${item.title}`} onClick={() => move(item, -1)}>
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveUpItem", { title: display.title })} onClick={() => move(item, -1)}>
<ArrowUpwardIcon fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Move down">
<Tooltip title={t("checklistMoveDown")}>
<span>
<IconButton size="small" disabled={busy} aria-label={`Move down: ${item.title}`} onClick={() => move(item, 1)}>
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveDownItem", { title: display.title })} onClick={() => move(item, 1)}>
<ArrowDownwardIcon fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title={item.isSystemGenerated ? "Not relevant for this role" : "Delete"}>
<Tooltip title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
<span>
<IconButton size="small" disabled={busy} aria-label={`Remove: ${item.title}`} onClick={() => remove(item)}>
<IconButton size="small" disabled={busy} aria-label={t("checklistRemoveItem", { title: display.title })} onClick={() => remove(item)}>
<DeleteOutlineIcon fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
</Stack>
</Stack>
))}
);
})}
</Stack>
</Paper>
))}
@@ -185,14 +190,34 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
<TextField
fullWidth
size="small"
label="Add your own task"
label={t("checklistAddTask")}
value={draft}
disabled={busy}
onChange={(e) => setDraft(e.target.value)}
/>
<Button type="submit" variant="contained" disabled={busy || !draft.trim()}>Add</Button>
<Button type="submit" variant="contained" disabled={busy || !draft.trim()}>{t("checklistAdd")}</Button>
</Stack>
</Paper>
</Stack>
);
}
function checklistCategoryLabel(t: (key: any, vars?: Record<string, string | number>) => string, category: string) {
const keys: Record<string, string> = {
preparation: "checklistCategoryPreparation",
submission: "checklistCategorySubmission",
"follow-up": "checklistCategoryFollowUp",
interview: "checklistCategoryInterview",
custom: "checklistCategoryCustom",
};
return keys[category] ? t(keys[category]) : category;
}
function checklistItemDisplay(t: (key: any, vars?: Record<string, string | number>) => string, item: ChecklistItem) {
if (!item.systemKey || item.systemKey.startsWith("learning:")) return { title: item.title, description: item.description };
const token = item.systemKey.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
return {
title: t(`checklistItem_${token}`),
description: t(`checklistItem_${token}Description`),
};
}