refactor(career): Phase 1 increment — user-facing terminology + component split
UI-only restructuring of the Career Profile surface. No change to
database models, CareerProfiles schema, CvVariants, extraction APIs, AI
services, CV rendering, or public CV.
Terminology -> user-facing (i18n strings):
- "Structured CV editor" -> "Career information"
- "CV structure overview" -> "Profile sections"
- "Summary bullets" -> "Professional summary"
- "Core skills" -> "Skills"
- "Analyze sections" -> "Read sections"
- "Original extraction" -> "Original import"
- hardcoded "Master career profile" -> "Career profile"
Help text de-jargoned; the Career information help now frames it as the
source the CV Builder consumes.
Component split (first step): extract ProfileCompleteness (completeness
meter + missing chips + version history) into src/views/career/. Display
only, props in, no state or API.
Save path untouched: api.put("/career/profile", { profile, cvText }). A
new test pins that exact call as the refactor invariant so the remaining
section extraction cannot silently change save behaviour. Existing
profile-page tests re-pointed to the new labels; every behavioural
assertion (save, parse, field values) kept.
Verified: tsc clean, production build clean, 136 frontend tests pass
(135 + 1 invariant). Sidebar fix from the previous task still passes.
Backend untouched.
The remaining Phase 1 work (per-section editor components, hiding the
template-driven builder and structure-overview blocks, actionable
per-section empty states) is staged in docs/career-workspace-ux-refactor.md
because it touches the live extraction test surface and is best verified
by driving the authenticated UI. This increment is a clean, non-regressing
checkpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import AuthStatusCard from "../components/AuthStatusCard";
|
||||
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
||||
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
||||
import CropImageDialog from "../components/CropImageDialog";
|
||||
import ProfileCompleteness from "./career/ProfileCompleteness";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import {
|
||||
@@ -498,51 +499,12 @@ export default function CareerProfilePage() {
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: 2.5, borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
{completeness ? (
|
||||
<Box sx={{ mb: 2.5, p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 2, flexWrap: "wrap", mb: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 900 }}>Profile completeness</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{completeness.percent}%</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={completeness.percent}
|
||||
color={completeness.percent >= 80 ? "success" : completeness.percent >= 40 ? "primary" : "warning"}
|
||||
sx={{ height: 8, borderRadius: 999 }}
|
||||
/>
|
||||
{completeness.missing.length > 0 ? (
|
||||
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", alignItems: "center", mt: 1.25 }}>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>Missing:</Typography>
|
||||
{completeness.missing.map((label) => (
|
||||
<Chip key={label} size="small" label={label} sx={{ height: 22, fontWeight: 700 }} />
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "success.main", mt: 1.25, fontWeight: 700 }}>Your master profile is complete.</Typography>
|
||||
)}
|
||||
{versions.length > 1 ? (
|
||||
<Accordion disableGutters elevation={0} sx={{ mt: 1.5, "&:before": { display: "none" }, backgroundColor: "transparent" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0, minHeight: 0 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>Version history ({versions.length})</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: 0, pt: 0 }}>
|
||||
<Box sx={{ display: "grid", gap: 0.75 }}>
|
||||
{versions.slice(0, 12).map((v) => (
|
||||
<Box key={v.version} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
v{v.version} · {v.source} · {new Date(v.createdAtUtc).toLocaleString()}{v.isCurrent ? " · current" : ""}
|
||||
</Typography>
|
||||
{!v.isCurrent ? (
|
||||
<Button size="small" variant="text" disabled={loading} onClick={() => void restoreVersion(v.version)}>Restore</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
) : null}
|
||||
</Box>
|
||||
) : null}
|
||||
<ProfileCompleteness
|
||||
completeness={completeness}
|
||||
versions={versions}
|
||||
loading={loading}
|
||||
onRestore={(version) => void restoreVersion(version)}
|
||||
/>
|
||||
<CropImageDialog
|
||||
open={cropOpen}
|
||||
file={avatarFile}
|
||||
@@ -626,7 +588,7 @@ export default function CareerProfilePage() {
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900 }}>
|
||||
{careerOnly ? "Master career profile" : t("profileTitle")}
|
||||
{careerOnly ? "Career profile" : t("profileTitle")}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>{me?.userName || me?.displayName || fullName || me?.email || "-"}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{headline || t("profileHeadlinePlaceholder")}</Typography>
|
||||
|
||||
Reference in New Issue
Block a user