Harden production schema fallback and profile/dashboard UI
This commit is contained in:
@@ -231,13 +231,13 @@ export default function DashboardView() {
|
||||
>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "flex-start" }}>
|
||||
<Box sx={{ maxWidth: 760 }}>
|
||||
<Typography variant="overline" sx={{ color: "primary.main", fontWeight: 800 }}>
|
||||
<Typography variant="overline" sx={{ color: theme.palette.mode === "dark" ? alpha(theme.palette.primary.light, 0.95) : "primary.main", fontWeight: 800 }}>
|
||||
{t("dashboardHeroLabel")}
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6, color: theme.palette.mode === "dark" ? "common.white" : "text.primary" }}>
|
||||
{t("dashboardOverviewTitle")}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: "text.secondary", mt: 1.25, maxWidth: 680 }}>
|
||||
<Typography variant="body1" sx={{ color: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.82) : "text.secondary", mt: 1.25, maxWidth: 680 }}>
|
||||
{t("dashboardOverviewBody")}
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ export const translations = {
|
||||
cropDialogZoom: "Zoom",
|
||||
cropDialogSave: "Save image",
|
||||
dashboardOverviewTitle: "Dashboard overview",
|
||||
dashboardHeroLabel: "Jobbjakt Analytics",
|
||||
dashboardHeroLabel: "Job search overview",
|
||||
dashboardResponseRate: "{rate}% response rate",
|
||||
dashboardMonthsShort: "{count} mo",
|
||||
dashboardAppliedCount: "{count} applied",
|
||||
@@ -243,7 +243,7 @@ export const translations = {
|
||||
dashboardResponseConversion: "{responses}/{total} response conversion",
|
||||
dashboardNoSourceData: "No source data yet.",
|
||||
dashboardCompanyJobsResponses: "{jobs} jobs · {responses} responses",
|
||||
dashboardOverviewBody: "High-level application activity only. System health and pipeline diagnostics now live in the System page to avoid duplicated or conflicting status data.",
|
||||
dashboardOverviewBody: "A quick view of your application activity, follow-ups, and momentum.",
|
||||
dashboardCustomize: "Customize dashboard",
|
||||
dashboardSummaryCards: "Summary cards",
|
||||
dashboardActivityChart: "Activity chart",
|
||||
@@ -1046,7 +1046,7 @@ export const translations = {
|
||||
cropDialogZoom: "Zoom",
|
||||
cropDialogSave: "Lagre bilde",
|
||||
dashboardOverviewTitle: "Dashboard-oversikt",
|
||||
dashboardHeroLabel: "Jobbjakt Analyse",
|
||||
dashboardHeroLabel: "Oversikt over jobbsøket",
|
||||
dashboardResponseRate: "{rate}% svarrate",
|
||||
dashboardMonthsShort: "{count} md",
|
||||
dashboardAppliedCount: "{count} søkt",
|
||||
@@ -1054,7 +1054,7 @@ export const translations = {
|
||||
dashboardResponseConversion: "{responses}/{total} svar-konvertering",
|
||||
dashboardNoSourceData: "Ingen kildedata ennå.",
|
||||
dashboardCompanyJobsResponses: "{jobs} jobber · {responses} svar",
|
||||
dashboardOverviewBody: "Kun overordnet aktivitet for jobbsøking vises her. Systemhelse og pipelinediagnostikk ligger nå på Systemsiden for å unngå dupliserte eller motstridende statusdata.",
|
||||
dashboardOverviewBody: "En rask oversikt over aktivitet, oppfølginger og fremdrift i jobbsøket ditt.",
|
||||
dashboardCustomize: "Tilpass dashboard",
|
||||
dashboardSummaryCards: "Oppsummeringskort",
|
||||
dashboardActivityChart: "Aktivitetsgraf",
|
||||
|
||||
@@ -118,7 +118,16 @@ export default function ProfilePage() {
|
||||
setProfileCvText(r.data?.profileCvText ?? "");
|
||||
try {
|
||||
const parsed = r.data?.profileCvStructureJson ? JSON.parse(r.data.profileCvStructureJson) : [];
|
||||
setParsedCvSections(Array.isArray(parsed) ? parsed : []);
|
||||
const normalized = Array.isArray(parsed)
|
||||
? parsed.map((section: any) => {
|
||||
const content = typeof section?.content === "string" ? section.content : "";
|
||||
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
|
||||
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
|
||||
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
|
||||
return { name, content, wordCount };
|
||||
})
|
||||
: [];
|
||||
setParsedCvSections(normalized);
|
||||
} catch {
|
||||
setParsedCvSections([]);
|
||||
}
|
||||
@@ -363,7 +372,16 @@ export default function ProfilePage() {
|
||||
setParsingCvSections(true);
|
||||
try {
|
||||
const res = await api.post<{ sections?: ParsedCvSection[] }>("/profile-cv/parse", { text: profileCvText });
|
||||
setParsedCvSections(res.data?.sections ?? []);
|
||||
const normalized = Array.isArray(res.data?.sections)
|
||||
? res.data.sections.map((section: any) => {
|
||||
const content = typeof section?.content === "string" ? section.content : "";
|
||||
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
|
||||
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
|
||||
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
|
||||
return { name, content, wordCount };
|
||||
})
|
||||
: [];
|
||||
setParsedCvSections(normalized);
|
||||
toast(t("profileCvStructureParsed"), "success");
|
||||
} catch (e: any) {
|
||||
toast(String(e?.response?.data || e?.message || t("profileCvStructureParseFailed")), "error");
|
||||
@@ -377,15 +395,18 @@ export default function ProfilePage() {
|
||||
</Box>
|
||||
{parsedCvSections.length > 0 ? (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||
{parsedCvSections.map((section) => (
|
||||
{parsedCvSections.map((section) => {
|
||||
const safeContent = typeof section.content === "string" ? section.content : "";
|
||||
const safeWordCount = Number.isFinite(Number(section.wordCount)) ? Number(section.wordCount) : (safeContent.trim() ? safeContent.trim().split(/\s+/).length : 0);
|
||||
return (
|
||||
<Box key={section.name} sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 0.75 }}>
|
||||
<Typography variant="overline">{section.name}</Typography>
|
||||
<Chip size="small" label={t("profileCvSectionWordCount", { count: section.wordCount })} />
|
||||
<Chip size="small" label={t("profileCvSectionWordCount", { count: safeWordCount })} />
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{section.content.slice(0, 280)}{section.content.length > 280 ? "…" : ""}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{safeContent.slice(0, 280)}{safeContent.length > 280 ? "…" : ""}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
)})}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureEmpty")}</Typography>
|
||||
|
||||
Reference in New Issue
Block a user