feat(i18n): localise career workspace overview
This commit is contained in:
@@ -4,6 +4,7 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|||||||
|
|
||||||
import { cvBuilderApi } from "./cvBuilder";
|
import { cvBuilderApi } from "./cvBuilder";
|
||||||
import CareerWorkspaceOverview, { CareerWorkspaceImportRun } from "./views/career/CareerWorkspaceOverview";
|
import CareerWorkspaceOverview, { CareerWorkspaceImportRun } from "./views/career/CareerWorkspaceOverview";
|
||||||
|
import { I18nProvider } from "./i18n/I18nProvider";
|
||||||
|
|
||||||
jest.mock("./cvBuilder", () => ({
|
jest.mock("./cvBuilder", () => ({
|
||||||
cvBuilderApi: { list: jest.fn() },
|
cvBuilderApi: { list: jest.fn() },
|
||||||
@@ -25,20 +26,24 @@ function renderOverview({
|
|||||||
loadError?: string | null;
|
loadError?: string | null;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return render(
|
return render(
|
||||||
|
<I18nProvider>
|
||||||
<CareerWorkspaceOverview
|
<CareerWorkspaceOverview
|
||||||
completeness={{ percent, missing }}
|
completeness={{ percent, missing }}
|
||||||
runs={runs}
|
runs={runs}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadError={loadError}
|
loadError={loadError}
|
||||||
/>,
|
/>
|
||||||
|
</I18nProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
window.localStorage.removeItem("uiLanguage");
|
||||||
mockedList.mockResolvedValue([]);
|
mockedList.mockResolvedValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
window.localStorage.removeItem("uiLanguage");
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +60,17 @@ test("first-run workspace presents concise profile, import, general CV and job-s
|
|||||||
expect(await screen.findByText("No CV documents yet.")).toBeInTheDocument();
|
expect(await screen.findByText("No CV documents yet.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("career workspace overview and import state render in Bokmål", async () => {
|
||||||
|
window.localStorage.setItem("uiLanguage", "nb");
|
||||||
|
renderOverview({ runs: [{ id: 7, status: "pending_review" }] });
|
||||||
|
|
||||||
|
expect(screen.getByRole("heading", { name: "Karriereområde", level: 1 })).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("Se gjennom importert CV", { selector: "a" })).toHaveLength(2);
|
||||||
|
expect(screen.getByText("Én import venter på godkjenning.")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("navigation", { name: "Handlinger i karriereområdet" })).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText("Ingen CV-dokumenter ennå.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("returning workspace shows missing profile information and recent general and job-specific CVs", async () => {
|
test("returning workspace shows missing profile information and recent general and job-specific CVs", async () => {
|
||||||
mockedList.mockResolvedValue([
|
mockedList.mockResolvedValue([
|
||||||
{ id: 8, name: "Backend CV", themeId: "modern", publicSlug: "backend", isPublic: false, version: 2, jobApplicationId: 42, updatedAtUtc: "2026-08-08T10:00:00Z" },
|
{ id: 8, name: "Backend CV", themeId: "modern", publicSlug: "backend", isPublic: false, version: 2, jobApplicationId: 42, updatedAtUtc: "2026-08-08T10:00:00Z" },
|
||||||
|
|||||||
@@ -427,6 +427,46 @@ export const translations = {
|
|||||||
cvAiGrammar: "Fix grammar",
|
cvAiGrammar: "Fix grammar",
|
||||||
cvAiImpact: "Add measurable impact",
|
cvAiImpact: "Add measurable impact",
|
||||||
cvAiBullets: "Suggest bullet points",
|
cvAiBullets: "Suggest bullet points",
|
||||||
|
careerOverviewTitle: "Career Workspace",
|
||||||
|
careerOverviewSubtitle: "Choose what you want to work on next.",
|
||||||
|
careerOverviewOpenBuilder: "Open CV Builder",
|
||||||
|
careerOverviewFirstRun: "Start by adding career information manually or importing a CV.",
|
||||||
|
careerOverviewUnavailable: "Workspace status is unavailable. The profile editor below can retry the request.",
|
||||||
|
careerOverviewProfileStatus: "Profile status",
|
||||||
|
careerOverviewCompleteness: "Profile completeness",
|
||||||
|
careerOverviewCompletenessLabel: "Career profile completeness",
|
||||||
|
careerOverviewAddNext: "Add next: {items}",
|
||||||
|
careerOverviewImproveProfile: "Improve profile",
|
||||||
|
careerOverviewCvImport: "CV import",
|
||||||
|
careerOverviewReviewImport: "Review imported CV",
|
||||||
|
careerOverviewReviewOne: "1 import is waiting for your approval.",
|
||||||
|
careerOverviewReviewMany: "{count} imports are waiting for your approval.",
|
||||||
|
careerOverviewViewProcessing: "View CV processing",
|
||||||
|
careerOverviewProcessingOne: "1 CV import is processing.",
|
||||||
|
careerOverviewProcessingMany: "{count} CV imports are processing.",
|
||||||
|
careerOverviewResolveImport: "Resolve CV import",
|
||||||
|
careerOverviewFailedOne: "1 CV import needs attention.",
|
||||||
|
careerOverviewFailedMany: "{count} CV imports need attention.",
|
||||||
|
careerOverviewImportCv: "Import a CV",
|
||||||
|
careerOverviewImportComplete: "Your latest CV import is complete.",
|
||||||
|
careerOverviewNoImport: "No CV has been imported yet.",
|
||||||
|
careerOverviewActions: "Career workspace actions",
|
||||||
|
careerOverviewCareerProfile: "Career profile",
|
||||||
|
careerOverviewCareerProfileBody: "Add or improve the facts used in every CV.",
|
||||||
|
careerOverviewEditProfile: "Edit career profile",
|
||||||
|
careerOverviewImportBody: "Extract information, then review every proposed change before applying it.",
|
||||||
|
careerOverviewGeneralCv: "General CV",
|
||||||
|
careerOverviewGeneralCvBody: "Create and manage reusable CV documents from your profile.",
|
||||||
|
careerOverviewCreateGeneralCv: "Create a general CV",
|
||||||
|
careerOverviewJobCv: "Job-specific CV",
|
||||||
|
careerOverviewJobCvBody: "Open a saved job to tailor and attach a CV for that application.",
|
||||||
|
careerOverviewChooseJob: "Choose a job",
|
||||||
|
careerOverviewRecentCvs: "Recent CVs",
|
||||||
|
careerOverviewViewAll: "View all CVs",
|
||||||
|
careerOverviewRecentFailed: "Recent CVs could not be loaded. Open the CV Builder to try again.",
|
||||||
|
careerOverviewNoCvs: "No CV documents yet.",
|
||||||
|
careerOverviewUpdated: "Updated {date}",
|
||||||
|
careerOverviewJobSpecific: "Job-specific",
|
||||||
richTextBold: "Bold",
|
richTextBold: "Bold",
|
||||||
richTextBoldPlaceholder: "bold text",
|
richTextBoldPlaceholder: "bold text",
|
||||||
richTextItalic: "Italic",
|
richTextItalic: "Italic",
|
||||||
@@ -2053,6 +2093,46 @@ export const translations = {
|
|||||||
cvAiGrammar: "Rett grammatikk",
|
cvAiGrammar: "Rett grammatikk",
|
||||||
cvAiImpact: "Legg til målbar effekt",
|
cvAiImpact: "Legg til målbar effekt",
|
||||||
cvAiBullets: "Foreslå punkter",
|
cvAiBullets: "Foreslå punkter",
|
||||||
|
careerOverviewTitle: "Karriereområde",
|
||||||
|
careerOverviewSubtitle: "Velg hva du vil arbeide med videre.",
|
||||||
|
careerOverviewOpenBuilder: "Åpne CV-bygger",
|
||||||
|
careerOverviewFirstRun: "Start med å legge til karriereinformasjon manuelt eller importere en CV.",
|
||||||
|
careerOverviewUnavailable: "Status for karriereområdet er utilgjengelig. Profilredigeringen nedenfor kan prøve forespørselen på nytt.",
|
||||||
|
careerOverviewProfileStatus: "Profilstatus",
|
||||||
|
careerOverviewCompleteness: "Hvor komplett profilen er",
|
||||||
|
careerOverviewCompletenessLabel: "Hvor komplett karriereprofilen er",
|
||||||
|
careerOverviewAddNext: "Legg til neste: {items}",
|
||||||
|
careerOverviewImproveProfile: "Forbedre profil",
|
||||||
|
careerOverviewCvImport: "CV-import",
|
||||||
|
careerOverviewReviewImport: "Se gjennom importert CV",
|
||||||
|
careerOverviewReviewOne: "Én import venter på godkjenning.",
|
||||||
|
careerOverviewReviewMany: "{count} importer venter på godkjenning.",
|
||||||
|
careerOverviewViewProcessing: "Vis CV-behandling",
|
||||||
|
careerOverviewProcessingOne: "Én CV-import behandles.",
|
||||||
|
careerOverviewProcessingMany: "{count} CV-importer behandles.",
|
||||||
|
careerOverviewResolveImport: "Løs problem med CV-import",
|
||||||
|
careerOverviewFailedOne: "Én CV-import trenger oppmerksomhet.",
|
||||||
|
careerOverviewFailedMany: "{count} CV-importer trenger oppmerksomhet.",
|
||||||
|
careerOverviewImportCv: "Importer en CV",
|
||||||
|
careerOverviewImportComplete: "Den siste CV-importen er fullført.",
|
||||||
|
careerOverviewNoImport: "Ingen CV er importert ennå.",
|
||||||
|
careerOverviewActions: "Handlinger i karriereområdet",
|
||||||
|
careerOverviewCareerProfile: "Karriereprofil",
|
||||||
|
careerOverviewCareerProfileBody: "Legg til eller forbedre fakta som brukes i alle CV-er.",
|
||||||
|
careerOverviewEditProfile: "Rediger karriereprofil",
|
||||||
|
careerOverviewImportBody: "Trekk ut informasjon, og se deretter gjennom alle foreslåtte endringer før de brukes.",
|
||||||
|
careerOverviewGeneralCv: "Generell CV",
|
||||||
|
careerOverviewGeneralCvBody: "Opprett og administrer gjenbrukbare CV-dokumenter fra profilen din.",
|
||||||
|
careerOverviewCreateGeneralCv: "Opprett en generell CV",
|
||||||
|
careerOverviewJobCv: "Jobbspesifikk CV",
|
||||||
|
careerOverviewJobCvBody: "Åpne en lagret jobb for å tilpasse og koble en CV til søknaden.",
|
||||||
|
careerOverviewChooseJob: "Velg en jobb",
|
||||||
|
careerOverviewRecentCvs: "Nylige CV-er",
|
||||||
|
careerOverviewViewAll: "Vis alle CV-er",
|
||||||
|
careerOverviewRecentFailed: "Nylige CV-er kunne ikke lastes. Åpne CV-byggeren for å prøve igjen.",
|
||||||
|
careerOverviewNoCvs: "Ingen CV-dokumenter ennå.",
|
||||||
|
careerOverviewUpdated: "Oppdatert {date}",
|
||||||
|
careerOverviewJobSpecific: "Jobbspesifikk",
|
||||||
richTextBold: "Fet",
|
richTextBold: "Fet",
|
||||||
richTextBoldPlaceholder: "fet tekst",
|
richTextBoldPlaceholder: "fet tekst",
|
||||||
richTextItalic: "Kursiv",
|
richTextItalic: "Kursiv",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import WorkOutlineIcon from "@mui/icons-material/WorkOutline";
|
|||||||
|
|
||||||
import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder";
|
import { CvVariantSummary, cvBuilderApi } from "../../cvBuilder";
|
||||||
import type { UserOperation } from "../../types";
|
import type { UserOperation } from "../../types";
|
||||||
|
import { useI18n } from "../../i18n/I18nProvider";
|
||||||
|
|
||||||
export type CareerWorkspaceCompleteness = {
|
export type CareerWorkspaceCompleteness = {
|
||||||
percent: number;
|
percent: number;
|
||||||
@@ -54,6 +55,7 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadError: string | null;
|
loadError: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const { language, t } = useI18n();
|
||||||
const [recentCvs, setRecentCvs] = useState<CvVariantSummary[]>([]);
|
const [recentCvs, setRecentCvs] = useState<CvVariantSummary[]>([]);
|
||||||
const [recentLoading, setRecentLoading] = useState(true);
|
const [recentLoading, setRecentLoading] = useState(true);
|
||||||
const [recentError, setRecentError] = useState(false);
|
const [recentError, setRecentError] = useState(false);
|
||||||
@@ -82,11 +84,11 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
|
|||||||
const review = runs.filter((run) => run.status === "pending_review");
|
const review = runs.filter((run) => run.status === "pending_review");
|
||||||
const active = runs.filter(isActive);
|
const active = runs.filter(isActive);
|
||||||
const failed = runs.filter((run) => run.status === "failed" || run.operation?.status === "failed");
|
const failed = runs.filter((run) => run.status === "failed" || run.operation?.status === "failed");
|
||||||
if (review.length > 0) return { severity: "warning" as const, label: "Review imported CV", message: `${review.length} import${review.length === 1 ? " is" : "s are"} waiting for your approval.` };
|
if (review.length > 0) return { severity: "warning" as const, label: t("careerOverviewReviewImport"), message: t(review.length === 1 ? "careerOverviewReviewOne" : "careerOverviewReviewMany", { count: review.length }) };
|
||||||
if (active.length > 0) return { severity: "info" as const, label: "View CV processing", message: `${active.length} CV import${active.length === 1 ? " is" : "s are"} processing.` };
|
if (active.length > 0) return { severity: "info" as const, label: t("careerOverviewViewProcessing"), message: t(active.length === 1 ? "careerOverviewProcessingOne" : "careerOverviewProcessingMany", { count: active.length }) };
|
||||||
if (failed.length > 0) return { severity: "error" as const, label: "Resolve CV import", message: `${failed.length} CV import${failed.length === 1 ? " needs" : "s need"} attention.` };
|
if (failed.length > 0) return { severity: "error" as const, label: t("careerOverviewResolveImport"), message: t(failed.length === 1 ? "careerOverviewFailedOne" : "careerOverviewFailedMany", { count: failed.length }) };
|
||||||
return { severity: "success" as const, label: "Import a CV", message: runs.length > 0 ? "Your latest CV import is complete." : "No CV has been imported yet." };
|
return { severity: "success" as const, label: t("careerOverviewImportCv"), message: t(runs.length > 0 ? "careerOverviewImportComplete" : "careerOverviewNoImport") };
|
||||||
}, [runs]);
|
}, [language, runs, t]);
|
||||||
|
|
||||||
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;
|
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;
|
||||||
|
|
||||||
@@ -95,65 +97,65 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
|
|||||||
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 4 }}>
|
<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 sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2, flexWrap: "wrap" }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography id="career-workspace-title" component="h1" variant="h5" sx={{ fontWeight: 900 }}>Career Workspace</Typography>
|
<Typography id="career-workspace-title" component="h1" variant="h5" sx={{ fontWeight: 900 }}>{t("careerOverviewTitle")}</Typography>
|
||||||
<Typography color="text.secondary">Choose what you want to work on next.</Typography>
|
<Typography color="text.secondary">{t("careerOverviewSubtitle")}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Button variant="contained" startIcon={<DescriptionOutlinedIcon />} href="/career/builder">Open CV Builder</Button>
|
<Button variant="contained" startIcon={<DescriptionOutlinedIcon />} href="/career/builder">{t("careerOverviewOpenBuilder")}</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{isFirstRun ? <Alert severity="info" sx={{ mt: 2 }}>Start by adding career information manually or importing a CV.</Alert> : null}
|
{isFirstRun ? <Alert severity="info" sx={{ mt: 2 }}>{t("careerOverviewFirstRun")}</Alert> : null}
|
||||||
{loadError ? <Alert severity="error" sx={{ mt: 2 }}>Workspace status is unavailable. The profile editor below can retry the request.</Alert> : null}
|
{loadError ? <Alert severity="error" sx={{ mt: 2 }}>{t("careerOverviewUnavailable")}</Alert> : null}
|
||||||
|
|
||||||
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr", md: "minmax(0, 1fr) minmax(0, 1fr)" }, gap: 2 }}>
|
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr", md: "minmax(0, 1fr) minmax(0, 1fr)" }, gap: 2 }}>
|
||||||
<Box aria-label="Profile status" sx={{ p: 2, borderRadius: 3, bgcolor: "background.default", border: "1px solid", borderColor: "divider" }}>
|
<Box aria-label={t("careerOverviewProfileStatus")} sx={{ p: 2, borderRadius: 3, bgcolor: "background.default", border: "1px solid", borderColor: "divider" }}>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center" }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>Profile completeness</Typography>
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("careerOverviewCompleteness")}</Typography>
|
||||||
{loading ? <Skeleton width={42} /> : <Typography sx={{ fontWeight: 900 }}>{completeness?.percent ?? 0}%</Typography>}
|
{loading ? <Skeleton width={42} /> : <Typography sx={{ fontWeight: 900 }}>{completeness?.percent ?? 0}%</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
<LinearProgress aria-label="Career profile completeness" variant={loading ? "indeterminate" : "determinate"} value={completeness?.percent ?? 0} sx={{ mt: 1, height: 8, borderRadius: 999 }} />
|
<LinearProgress aria-label={t("careerOverviewCompletenessLabel")} variant={loading ? "indeterminate" : "determinate"} value={completeness?.percent ?? 0} sx={{ mt: 1, height: 8, borderRadius: 999 }} />
|
||||||
{!loading && completeness?.missing.length ? (
|
{!loading && completeness?.missing.length ? (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>Add next: {completeness.missing.slice(0, 3).join(", ")}</Typography>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("careerOverviewAddNext", { items: completeness.missing.slice(0, 3).join(", ") })}</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Button href="#career-profile-editor" size="small" sx={{ mt: 1, px: 0.5 }}>Improve profile</Button>
|
<Button href="#career-profile-editor" size="small" sx={{ mt: 1, px: 0.5 }}>{t("careerOverviewImproveProfile")}</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Alert severity={importState.severity} sx={{ borderRadius: 3, alignItems: "flex-start" }}>
|
<Alert severity={importState.severity} sx={{ borderRadius: 3, alignItems: "flex-start" }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>CV import</Typography>
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("careerOverviewCvImport")}</Typography>
|
||||||
<Typography variant="body2">{importState.message}</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-cv-import" color="inherit" size="small" sx={{ mt: 0.75, px: 0.5 }}>{importState.label}</Button>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Box component="nav" aria-label="Career workspace actions" sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(2, minmax(0, 1fr))", xl: "repeat(4, minmax(0, 1fr))" }, gap: 1.5 }}>
|
<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="Career profile" body="Add or improve the facts used in every CV." href="#career-profile-editor" label="Edit career profile" icon={<PersonOutlineIcon />} />
|
<ActionCard title={t("careerOverviewCareerProfile")} body={t("careerOverviewCareerProfileBody")} href="#career-profile-editor" label={t("careerOverviewEditProfile")} icon={<PersonOutlineIcon />} />
|
||||||
<ActionCard title="Import a CV" body="Extract information, then review every proposed change before applying it." href="#career-cv-import" label={importState.label} icon={<UploadFileOutlinedIcon />} />
|
<ActionCard title={t("careerOverviewImportCv")} body={t("careerOverviewImportBody")} href="#career-cv-import" label={importState.label} icon={<UploadFileOutlinedIcon />} />
|
||||||
<ActionCard title="General CV" body="Create and manage reusable CV documents from your profile." href="/career/builder" label="Create a general CV" icon={<DescriptionOutlinedIcon />} />
|
<ActionCard title={t("careerOverviewGeneralCv")} body={t("careerOverviewGeneralCvBody")} href="/career/builder" label={t("careerOverviewCreateGeneralCv")} icon={<DescriptionOutlinedIcon />} />
|
||||||
<ActionCard title="Job-specific CV" body="Open a saved job to tailor and attach a CV for that application." href="/jobs" label="Choose a job" icon={<WorkOutlineIcon />} />
|
<ActionCard title={t("careerOverviewJobCv")} body={t("careerOverviewJobCvBody")} href="/jobs" label={t("careerOverviewChooseJob")} icon={<WorkOutlineIcon />} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper component="section" aria-labelledby="recent-cvs-title" sx={{ p: 2, borderRadius: 4 }}>
|
<Paper component="section" aria-labelledby="recent-cvs-title" sx={{ p: 2, borderRadius: 4 }}>
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
||||||
<Typography id="recent-cvs-title" variant="h6" sx={{ fontWeight: 800 }}>Recent CVs</Typography>
|
<Typography id="recent-cvs-title" variant="h6" sx={{ fontWeight: 800 }}>{t("careerOverviewRecentCvs")}</Typography>
|
||||||
<Button href="/career/builder" size="small">View all CVs</Button>
|
<Button href="/career/builder" size="small">{t("careerOverviewViewAll")}</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{recentLoading ? (
|
{recentLoading ? (
|
||||||
<Stack spacing={1} sx={{ mt: 1.5 }}><Skeleton height={32} /><Skeleton height={32} /></Stack>
|
<Stack spacing={1} sx={{ mt: 1.5 }}><Skeleton height={32} /><Skeleton height={32} /></Stack>
|
||||||
) : recentError ? (
|
) : recentError ? (
|
||||||
<Alert severity="warning" sx={{ mt: 1.5 }}>Recent CVs could not be loaded. Open the CV Builder to try again.</Alert>
|
<Alert severity="warning" sx={{ mt: 1.5 }}>{t("careerOverviewRecentFailed")}</Alert>
|
||||||
) : recentCvs.length === 0 ? (
|
) : recentCvs.length === 0 ? (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>No CV documents yet.</Typography>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("careerOverviewNoCvs")}</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Stack spacing={1} sx={{ mt: 1 }}>
|
<Stack spacing={1} sx={{ mt: 1 }}>
|
||||||
{recentCvs.map((cv) => (
|
{recentCvs.map((cv) => (
|
||||||
<Box key={cv.id} sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", flexWrap: "wrap" }}>
|
<Box key={cv.id} sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", flexWrap: "wrap" }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>{cv.name}</Typography>
|
<Typography variant="body2" sx={{ fontWeight: 700 }}>{cv.name}</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">Updated {new Date(cv.updatedAtUtc).toLocaleDateString()}</Typography>
|
<Typography variant="caption" color="text.secondary">{t("careerOverviewUpdated", { date: new Date(cv.updatedAtUtc).toLocaleDateString(language === "nb" ? "nb-NO" : "en-GB") })}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", gap: 0.75, alignItems: "center" }}>
|
<Box sx={{ display: "flex", gap: 0.75, alignItems: "center" }}>
|
||||||
{cv.jobApplicationId ? <Chip label="Job-specific" size="small" variant="outlined" /> : null}
|
{cv.jobApplicationId ? <Chip label={t("careerOverviewJobSpecific")} size="small" variant="outlined" /> : null}
|
||||||
<Button href={`/career/builder/${cv.id}`} size="small">Open</Button>
|
<Button href={`/career/builder/${cv.id}`} size="small">{t("cvDashboardOpen")}</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user