Track cleanup progress and polish profile/system flows
This commit is contained in:
@@ -37,7 +37,7 @@ export default function AuthStatusCard() {
|
||||
.catch(() => setMe(null));
|
||||
}, [token]);
|
||||
|
||||
const label = useMemo(() => me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email, [me]);
|
||||
const label = useMemo(() => me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.userName || me?.email, [me]);
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
|
||||
@@ -27,7 +27,7 @@ import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
|
||||
import MailOutlineIcon from "@mui/icons-material/MailOutline";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
@@ -126,8 +126,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
});
|
||||
setGmailMessages(res.data);
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to load Gmail messages.";
|
||||
toast(message, "error");
|
||||
toast(getApiErrorMessage(error, "Failed to load Gmail messages."), "error");
|
||||
} finally {
|
||||
setGmailMessagesLoading(false);
|
||||
}
|
||||
@@ -206,8 +205,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
await api.post("/correspondence", { jobApplicationId: jobId, from, content: text });
|
||||
setText("");
|
||||
await load();
|
||||
} catch {
|
||||
toast("Failed to add message.", "error");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to add message."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,8 +230,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
setRawEmail("");
|
||||
await load();
|
||||
toast("Email logged.", "success");
|
||||
} catch {
|
||||
toast("Failed to import email.", "error");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to import email."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,8 +240,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
const res = await api.get<{ url: string }>("/gmail/connect-url");
|
||||
const popup = window.open(res.data.url, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes");
|
||||
if (!popup) toast("Your browser blocked the Gmail popup.", "error");
|
||||
} catch {
|
||||
toast("Failed to start Gmail connection.", "error");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to start Gmail connection."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,8 +251,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
setGmailStatus({ connected: false });
|
||||
setGmailMessages([]);
|
||||
toast("Gmail disconnected.", "success");
|
||||
} catch {
|
||||
toast("Failed to disconnect Gmail.", "error");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,8 +262,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
await api.delete(`/correspondence/${messageId}`);
|
||||
await load();
|
||||
toast("Message removed.", "success");
|
||||
} catch {
|
||||
toast("Failed to remove message.", "error");
|
||||
} catch (error) {
|
||||
toast(getApiErrorMessage(error, "Failed to remove message."), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,8 +274,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
await load();
|
||||
toast("Email imported from Gmail.", "success");
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to import Gmail message.";
|
||||
toast(message, "error");
|
||||
toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error");
|
||||
} finally {
|
||||
setImportingMessageId(null);
|
||||
}
|
||||
@@ -289,8 +287,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
await load();
|
||||
toast(`Imported ${res.data.imported} messages${res.data.skipped ? `, skipped ${res.data.skipped} duplicates` : ""}.`, "success");
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to import Gmail thread.";
|
||||
toast(message, "error");
|
||||
toast(getApiErrorMessage(error, "Failed to import Gmail thread."), "error");
|
||||
} finally {
|
||||
setImportingThreadId(null);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ declare global {
|
||||
type MeResponse = {
|
||||
provider?: "local" | "google" | "external";
|
||||
email?: string;
|
||||
userName?: string;
|
||||
displayName?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
@@ -153,7 +154,7 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
|
||||
};
|
||||
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, isRawGoogleToken, token, toast, t]);
|
||||
|
||||
const signedInName = me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || "";
|
||||
const signedInName = me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.userName || me?.email || "";
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { ApplicationPackageResponse, CandidateFit, InterviewPrepResponse, JobApplication, ReadinessResponse } from "../types";
|
||||
import { useToast } from "../toast";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
@@ -216,7 +216,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setJob(res.data);
|
||||
toast("Summary and skills refreshed.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to refresh summary and skills.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to refresh summary and skills."), "error");
|
||||
} finally {
|
||||
setRefreshingAi(false);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setTailoredCvText(res.data.tailoredCvText ?? "");
|
||||
toast("Application package generated.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to generate application package.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to generate application package."), "error");
|
||||
} finally {
|
||||
setGeneratingPackage(false);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setInterviewPrep(null);
|
||||
toast("Tailored CV saved.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to save tailored CV.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to save tailored CV."), "error");
|
||||
} finally {
|
||||
setSavingTailoredCv(false);
|
||||
}
|
||||
@@ -317,7 +317,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setReadiness(null);
|
||||
toast("Cover letter saved to this job.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to save cover letter.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to save cover letter."), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
@@ -330,7 +330,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setReadiness(null);
|
||||
toast("Application answer saved to notes.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to save application answer.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to save application answer."), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
@@ -343,7 +343,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setJob((prev) => prev ? { ...prev, recruiterMessageDraft: content } : prev);
|
||||
toast("Recruiter message saved to this job.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to save recruiter message.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to save recruiter message."), "error");
|
||||
} finally {
|
||||
setSavingApplicationDrafts(false);
|
||||
}
|
||||
@@ -374,7 +374,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
setReadiness(null);
|
||||
toast("Follow-up sent and logged.", "success");
|
||||
} catch (error: any) {
|
||||
toast(error?.response?.data || "Failed to send follow-up.", "error");
|
||||
toast(getApiErrorMessage(error, "Failed to send follow-up."), "error");
|
||||
} finally {
|
||||
setSendingDraft(false);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export const translations = {
|
||||
profileHeadline: "Profile headline",
|
||||
profileHeadlineHelp: "Stored only in this browser to personalize your workspace.",
|
||||
profileMasterCv: "Master CV",
|
||||
profileMasterCvBody: "Upload a PDF, Word document, plain text file, markdown file, or image scan. Where supported, the app can extract text automatically and populate your master CV text for tailoring and outreach.",
|
||||
profileMasterCvBody: "Upload a PDF, DOCX, plain text file, or markdown file. The app extracts text where supported and populates your master CV text for tailoring and outreach.",
|
||||
profileUploadCv: "Upload CV",
|
||||
profileUploading: "Uploading...",
|
||||
profileCopyCvText: "Copy CV text",
|
||||
@@ -179,7 +179,7 @@ export const translations = {
|
||||
profileCvUploadFailed: "Failed to upload CV.",
|
||||
profileCvTextLabel: "Profile CV / master resume text",
|
||||
profileCvTextHelp: "Keep this updated and specific. Include recent roles, tools, achievements, measurable outcomes, and the work you want to be hired for next. If extraction misses something, edit it here manually.",
|
||||
profileCvPreferredUploads: "Preferred uploads: PDF, DOC, DOCX. Text and image files are also accepted.",
|
||||
profileCvPreferredUploads: "Supported uploads: PDF, DOCX, TXT, MD.",
|
||||
profileSaveChanges: "Save changes",
|
||||
profileUpdated: "Profile updated.",
|
||||
profileUpdateFailed: "Failed to update profile.",
|
||||
@@ -591,7 +591,7 @@ export const translations = {
|
||||
profileHeadline: "Profiloverskrift",
|
||||
profileHeadlineHelp: "Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.",
|
||||
profileMasterCv: "Hoved-CV",
|
||||
profileMasterCvBody: "Last opp en PDF, et Word-dokument, en ren tekstfil, en markdown-fil eller et bildeskann. Der det støttes kan appen automatisk hente ut tekst og fylle inn hoved-CV-en din for tilpasning og kontakt.",
|
||||
profileMasterCvBody: "Last opp en PDF, DOCX, ren tekstfil eller markdown-fil. Appen henter ut tekst der det støttes og fyller inn hoved-CV-en din for tilpasning og kontakt.",
|
||||
profileUploadCv: "Last opp CV",
|
||||
profileUploading: "Laster opp...",
|
||||
profileCopyCvText: "Kopier CV-tekst",
|
||||
@@ -599,7 +599,7 @@ export const translations = {
|
||||
profileCvUploadFailed: "Kunne ikke laste opp CV.",
|
||||
profileCvTextLabel: "Profil-CV / hovedtekst for CV",
|
||||
profileCvTextHelp: "Hold denne oppdatert og konkret. Ta med nylige roller, verktøy, prestasjoner, målbare resultater og arbeidet du vil bli ansatt for neste gang. Hvis tekstuttrekket mangler noe, kan du redigere manuelt her.",
|
||||
profileCvPreferredUploads: "Foretrukne opplastinger: PDF, DOC, DOCX. Tekst- og bildefiler aksepteres også.",
|
||||
profileCvPreferredUploads: "Støttede opplastinger: PDF, DOCX, TXT, MD.",
|
||||
profileSaveChanges: "Lagre endringer",
|
||||
profileUpdated: "Profil oppdatert.",
|
||||
profileUpdateFailed: "Kunne ikke oppdatere profil.",
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
type SummarizerMetrics = {
|
||||
@@ -130,7 +130,7 @@ export default function AdminSystemPage() {
|
||||
const res = await api.get<SystemStatus>("/admin/system");
|
||||
setStatus(res.data);
|
||||
} catch (e: any) {
|
||||
setError(e?.response?.data || e?.message || "Failed to load system status.");
|
||||
setError(getApiErrorMessage(e, "Failed to load system status."));
|
||||
setStatus(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -164,7 +164,7 @@ export default function AdminSystemPage() {
|
||||
message: testEmailMessage.trim() || null,
|
||||
});
|
||||
} catch (e: any) {
|
||||
setError(e?.response?.data || e?.message || "Failed to send test email.");
|
||||
setError(getApiErrorMessage(e, "Failed to send test email."));
|
||||
} finally {
|
||||
setSendingTestEmail(false);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export default function AdminSystemPage() {
|
||||
await api.post("/admin/system/summarizer/probe");
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setError(e?.response?.data || e?.message || "Failed to run summarizer probe.");
|
||||
setError(getApiErrorMessage(e, "Failed to run summarizer probe."));
|
||||
} finally {
|
||||
setRunningProbe(false);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type MeResponse = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
const CV_UPLOAD_ACCEPT = ".pdf,.doc,.docx,.txt,.md,image/*,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||
|
||||
function initialsFrom(values: Array<string | undefined>) {
|
||||
|
||||
@@ -285,6 +285,7 @@ export const getTheme = (_mode: "light" | "dark", accentColor: string) => {
|
||||
paddingRight: 0,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
lineHeight: 1.45,
|
||||
},
|
||||
inputMultiline: {
|
||||
paddingTop: 0,
|
||||
@@ -295,6 +296,11 @@ export const getTheme = (_mode: "light" | "dark", accentColor: string) => {
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
input: {
|
||||
"&::placeholder": {
|
||||
opacity: 0.72,
|
||||
},
|
||||
},
|
||||
inputMultiline: {
|
||||
"&::placeholder": {
|
||||
opacity: 0.72,
|
||||
|
||||
Reference in New Issue
Block a user