fix(i18n): finish career activity copy
This commit is contained in:
@@ -30,18 +30,22 @@ type MetadataLookup = (path: string) => StructuredCvFieldMetadata | undefined;
|
||||
// Field-review chip + tone, moved verbatim from CareerProfilePage so the sections and the parent
|
||||
// share one definition. Behaviour (thresholds, labels, source snippet) is unchanged.
|
||||
function confidenceTone(confidence?: number) {
|
||||
if (typeof confidence !== "number") return { label: "Review", color: "default" as const };
|
||||
if (confidence >= 0.8) return { label: `High ${Math.round(confidence * 100)}%`, color: "success" as const };
|
||||
if (confidence >= 0.65) return { label: `Medium ${Math.round(confidence * 100)}%`, color: "warning" as const };
|
||||
return { label: `Low ${Math.round(confidence * 100)}%`, color: "error" as const };
|
||||
if (typeof confidence !== "number") return { key: "profileCvConfidenceReview" as const, color: "default" as const };
|
||||
if (confidence >= 0.8) return { key: "profileCvConfidenceHigh" as const, color: "success" as const };
|
||||
if (confidence >= 0.65) return { key: "profileCvConfidenceMedium" as const, color: "warning" as const };
|
||||
return { key: "profileCvConfidenceLow" as const, color: "error" as const };
|
||||
}
|
||||
|
||||
export function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata }) {
|
||||
const { t } = useI18n();
|
||||
if (!metadata) return null;
|
||||
const tone = confidenceTone(metadata.confidence);
|
||||
const label = typeof metadata.confidence === "number"
|
||||
? t(tone.key, { percent: Math.round(metadata.confidence * 100) })
|
||||
: t(tone.key);
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", mt: 0.75, alignItems: "center" }}>
|
||||
<Chip size="small" color={tone.color} variant={tone.color === "default" ? "outlined" : "filled"} label={tone.label} />
|
||||
<Chip size="small" color={tone.color} variant={tone.color === "default" ? "outlined" : "filled"} label={label} />
|
||||
{metadata.method ? <Chip size="small" variant="outlined" label={metadata.method} /> : null}
|
||||
{metadata.sourceBlockId ? <Chip size="small" variant="outlined" label={metadata.sourceBlockId} /> : null}
|
||||
{metadata.reviewState ? <Chip size="small" variant="outlined" label={metadata.reviewState} /> : null}
|
||||
@@ -91,8 +95,8 @@ export function PersonalInformationSection({
|
||||
<TextField label={t("profileCvContactLinkedIn")} value={value.linkedIn ?? ""} onChange={(e) => set({ linkedIn: e.target.value || undefined })} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} />
|
||||
<TextField label="GitHub" value={value.gitHub ?? ""} onChange={(e) => set({ gitHub: e.target.value || undefined })} fullWidth sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} />
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1 }}><Typography variant="subtitle2">Other links</Typography><Button size="small" startIcon={<AddIcon />} onClick={() => set({ links: [...(value.links ?? []), { label: "", url: "" }] })}>Add link</Button></Stack>
|
||||
<Stack spacing={1}>{(value.links ?? []).map((link, index) => <Stack key={index} direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ sm: "center" }}><TextField size="small" label="Label" value={link.label ?? ""} onChange={(event) => set({ links: (value.links ?? []).map((item, itemIndex) => itemIndex === index ? { ...item, label: event.target.value || undefined } : item) })} sx={{ flex: "0 1 180px" }} /><TextField size="small" label="URL" value={link.url ?? ""} onChange={(event) => set({ links: (value.links ?? []).map((item, itemIndex) => itemIndex === index ? { ...item, url: event.target.value || undefined } : item) })} fullWidth /><IconButton aria-label={`Delete custom link ${index + 1}`} onClick={() => set({ links: (value.links ?? []).filter((_, itemIndex) => itemIndex !== index) })}><DeleteOutlineIcon fontSize="small" /></IconButton></Stack>)}</Stack>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1 }}><Typography variant="subtitle2">{t("profileCvOtherLinks")}</Typography><Button size="small" startIcon={<AddIcon />} onClick={() => set({ links: [...(value.links ?? []), { label: "", url: "" }] })}>{t("profileCvAddLink")}</Button></Stack>
|
||||
<Stack spacing={1}>{(value.links ?? []).map((link, index) => <Stack key={index} direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ sm: "center" }}><TextField size="small" label={t("profileCvLinkLabel")} value={link.label ?? ""} onChange={(event) => set({ links: (value.links ?? []).map((item, itemIndex) => itemIndex === index ? { ...item, label: event.target.value || undefined } : item) })} sx={{ flex: "0 1 180px" }} /><TextField size="small" label={t("profileCvLinkUrl")} value={link.url ?? ""} onChange={(event) => set({ links: (value.links ?? []).map((item, itemIndex) => itemIndex === index ? { ...item, url: event.target.value || undefined } : item) })} fullWidth /><IconButton aria-label={t("profileCvDeleteCustomLink", { index: index + 1 })} onClick={() => set({ links: (value.links ?? []).filter((_, itemIndex) => itemIndex !== index) })}><DeleteOutlineIcon fontSize="small" /></IconButton></Stack>)}</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import WorkOutlineIcon from "@mui/icons-material/WorkOutline";
|
||||
import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder";
|
||||
import type { UserOperation } from "../../types";
|
||||
import { useI18n } from "../../i18n/I18nProvider";
|
||||
import { localizeCompletenessLabel } from "./careerCompletenessLabels";
|
||||
|
||||
export type CareerWorkspaceCompleteness = {
|
||||
percent: number;
|
||||
@@ -123,7 +124,7 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
|
||||
</Box>
|
||||
<LinearProgress aria-label={t("careerOverviewCompletenessLabel")} variant={loading ? "indeterminate" : "determinate"} value={completeness?.percent ?? 0} sx={{ mt: 1, height: 8, borderRadius: 999 }} />
|
||||
{!loading && completeness?.missing.length ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).join(", ") })}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).map((label) => localizeCompletenessLabel(label, t)).join(", ") })}</Typography>
|
||||
) : null}
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Chip, LinearProgress, Typography } from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { useI18n } from "../../i18n/I18nProvider";
|
||||
import { localizeCompletenessLabel } from "./careerCompletenessLabels";
|
||||
|
||||
// Display-only slice of the Career Profile page: the completeness meter, the "missing" chips, and the
|
||||
// version-history accordion. Extracted from CareerProfilePage as the first step of the Phase 1
|
||||
@@ -31,14 +33,17 @@ export default function ProfileCompleteness({
|
||||
onRestore: (version: number) => void;
|
||||
showSummary?: boolean;
|
||||
}) {
|
||||
const { language, t } = useI18n();
|
||||
if ((!showSummary || !completeness) && versions.length <= 1) return null;
|
||||
|
||||
const locale = language === "nb" ? "nb-NO" : "en";
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 2.5, p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
{showSummary && completeness ? (
|
||||
<>
|
||||
<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="subtitle1" sx={{ fontWeight: 900 }}>{t("profileCvCompletenessTitle")}</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{completeness.percent}%</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
@@ -49,30 +54,30 @@ export default function ProfileCompleteness({
|
||||
/>
|
||||
{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>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvCompletenessMissing")}</Typography>
|
||||
{completeness.missing.map((label) => (
|
||||
<Chip key={label} size="small" label={label} sx={{ height: 22, fontWeight: 700 }} />
|
||||
<Chip key={label} size="small" label={localizeCompletenessLabel(label, t)} sx={{ height: 22, fontWeight: 700 }} />
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "success.main", mt: 1.25, fontWeight: 700 }}>Your career profile is complete.</Typography>
|
||||
<Typography variant="body2" sx={{ color: "success.main", mt: 1.25, fontWeight: 700 }}>{t("profileCvCompletenessComplete")}</Typography>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{versions.length > 1 ? (
|
||||
<Accordion disableGutters elevation={0} sx={{ mt: showSummary ? 1.5 : 0, "&:before": { display: "none" }, backgroundColor: "transparent" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0, minHeight: 0 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>Version history ({versions.length})</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>{t("profileCvVersionHistory", { count: 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" : ""}
|
||||
v{v.version} · {v.source} · {new Date(v.createdAtUtc).toLocaleString(locale)}{v.isCurrent ? ` · ${t("profileCvVersionCurrent")}` : ""}
|
||||
</Typography>
|
||||
{!v.isCurrent ? (
|
||||
<Button size="small" variant="text" disabled={loading} onClick={() => onRestore(v.version)}>Restore</Button>
|
||||
<Button size="small" variant="text" disabled={loading} onClick={() => onRestore(v.version)}>{t("profileCvVersionRestore")}</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { TranslationKey } from "../../i18n/translations";
|
||||
|
||||
const completenessLabelKeys: Record<string, TranslationKey> = {
|
||||
"Personal details": "profileCvCompletenessPersonal",
|
||||
"Professional summary": "profileCvCompletenessSummary",
|
||||
Experience: "profileCvCompletenessExperience",
|
||||
Education: "profileCvCompletenessEducation",
|
||||
Skills: "profileCvCompletenessSkills",
|
||||
Projects: "profileCvCompletenessProjects",
|
||||
Languages: "profileCvCompletenessLanguages",
|
||||
};
|
||||
|
||||
export function localizeCompletenessLabel(label: string, t: (key: TranslationKey) => string) {
|
||||
const key = completenessLabelKeys[label];
|
||||
return key ? t(key) : label;
|
||||
}
|
||||
Reference in New Issue
Block a user