refactor, security updates, cv extraction upgrades
This commit is contained in:
@@ -5,7 +5,7 @@ import { Box, Button, Checkbox, FormControlLabel, Paper, Tab, Tabs, TextField, T
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { getRememberMePref, setAuthToken, setRememberMePref } from "../auth";
|
||||
import { getRememberMePref, setAuthPersistencePreference } from "../auth";
|
||||
import GoogleAuthCard from "../components/GoogleAuthCard";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
@@ -44,9 +44,9 @@ export default function LoginPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const url = mode === "register" ? "/auth/register" : "/auth/login";
|
||||
const res = await api.post<{ accessToken: string; tokenType: string }>(url, { email, password });
|
||||
setRememberMePref(rememberMe);
|
||||
setAuthToken(res.data.accessToken, rememberMe ? "local" : "session");
|
||||
await api.post(url, { email, password, rememberMe });
|
||||
setAuthPersistencePreference(rememberMe ? "local" : "session");
|
||||
await api.get("/auth/me");
|
||||
toast(t("signedIn"), "success");
|
||||
navigate(nextPath, { replace: true });
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -42,6 +42,12 @@ type ExtractionRun = {
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
type QueuedCvRunResponse = {
|
||||
queued: boolean;
|
||||
extractionRunId: number;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type JobListResponse = {
|
||||
items: JobApplication[];
|
||||
total: number;
|
||||
@@ -199,6 +205,7 @@ export default function ProfilePage() {
|
||||
const avatarInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [me, setMe] = useState<MeResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [uploadingCv, setUploadingCv] = useState(false);
|
||||
const [improvingCv, setImprovingCv] = useState(false);
|
||||
const [rebuildingCv, setRebuildingCv] = useState(false);
|
||||
@@ -225,10 +232,12 @@ export default function ProfilePage() {
|
||||
const [reprocessingCv, setReprocessingCv] = useState(false);
|
||||
const [structuredCv, setStructuredCv] = useState<StructuredCvProfile>(emptyStructuredCv());
|
||||
const [extractionRuns, setExtractionRuns] = useState<ExtractionRun[]>([]);
|
||||
const runStatusRef = useRef<Record<number, string>>({});
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [profileResponse, runsResponse, jobsResponse] = await Promise.all([
|
||||
api.get<MeResponse>("/auth/me"),
|
||||
@@ -247,10 +256,14 @@ export default function ProfilePage() {
|
||||
setExtractionRuns(runsResponse.data ?? []);
|
||||
setSavedJobs(jobsResponse.data?.items ?? []);
|
||||
setHeadline(window.localStorage.getItem("profileHeadline") ?? "");
|
||||
} catch {
|
||||
setLoadError(null);
|
||||
} catch (error: any) {
|
||||
setMe(null);
|
||||
setExtractionRuns([]);
|
||||
setSavedJobs([]);
|
||||
setLoadError(String(error?.response?.data || error?.message || "Unable to load profile right now."));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -258,6 +271,31 @@ export default function ProfilePage() {
|
||||
void loadProfile();
|
||||
}, [loadProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeRuns = extractionRuns.filter((run) => run.status === "queued" || run.status === "running");
|
||||
if (activeRuns.length === 0) return;
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
void loadProfile();
|
||||
}, 4000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [extractionRuns, loadProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
const previous = runStatusRef.current;
|
||||
for (const run of extractionRuns) {
|
||||
const prior = previous[run.id];
|
||||
if ((prior === "queued" || prior === "running") && run.status === "applied") {
|
||||
toast(`CV ${run.trigger} completed.`, "success");
|
||||
}
|
||||
if ((prior === "queued" || prior === "running") && run.status === "failed") {
|
||||
toast(run.errorMessage || `CV ${run.trigger} failed.`, "error");
|
||||
}
|
||||
previous[run.id] = run.status;
|
||||
}
|
||||
}, [extractionRuns, toast]);
|
||||
|
||||
const initials = useMemo(() => initialsFrom([me?.displayName, me?.firstName, me?.lastName, me?.userName, me?.email]), [me]);
|
||||
const isLocal = me?.provider === "local";
|
||||
const fullName = [me?.firstName, me?.lastName].filter(Boolean).join(" ");
|
||||
@@ -305,6 +343,13 @@ export default function ProfilePage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{loadError ? (
|
||||
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>Retry</Button>}>
|
||||
Unable to load profile.
|
||||
<Typography variant="body2" sx={{ mt: 0.5 }}>{loadError}</Typography>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 1 }}>
|
||||
@@ -413,7 +458,7 @@ export default function ProfilePage() {
|
||||
formData.append("file", file);
|
||||
setUploadingCv(true);
|
||||
try {
|
||||
await api.post("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
|
||||
await api.post<QueuedCvRunResponse>("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
|
||||
await loadProfile();
|
||||
toast(t("profileCvUploaded"), "success");
|
||||
} catch (e: any) {
|
||||
@@ -432,10 +477,9 @@ export default function ProfilePage() {
|
||||
onClick={async () => {
|
||||
setRebuildingCv(true);
|
||||
try {
|
||||
const res = await api.post<{ text?: string }>("/profile-cv/rebuild");
|
||||
if (res.data?.text) setProfileCvText(res.data.text);
|
||||
const res = await api.post<QueuedCvRunResponse>("/profile-cv/rebuild");
|
||||
await loadProfile();
|
||||
toast(t("profileCvRebuilt"), "success");
|
||||
toast(`Queued CV rebuild (run ${res.data.extractionRunId}).`, "info");
|
||||
} catch (e: any) {
|
||||
toast(String(e?.response?.data || e?.message || t("profileCvRebuildFailed")), "error");
|
||||
} finally {
|
||||
@@ -451,10 +495,9 @@ export default function ProfilePage() {
|
||||
onClick={async () => {
|
||||
setImprovingCv(true);
|
||||
try {
|
||||
const res = await api.post<{ text?: string }>("/profile-cv/improve");
|
||||
if (res.data?.text) setProfileCvText(res.data.text);
|
||||
const res = await api.post<QueuedCvRunResponse>("/profile-cv/improve");
|
||||
await loadProfile();
|
||||
toast(t("profileCvImproved"), "success");
|
||||
toast(`Queued CV improve run (run ${res.data.extractionRunId}).`, "info");
|
||||
} catch (e: any) {
|
||||
toast(String(e?.response?.data || e?.message || t("profileCvImproveFailed")), "error");
|
||||
} finally {
|
||||
@@ -470,9 +513,9 @@ export default function ProfilePage() {
|
||||
onClick={async () => {
|
||||
setReprocessingCv(true);
|
||||
try {
|
||||
await api.post("/profile-cv/reprocess");
|
||||
const res = await api.post<QueuedCvRunResponse>("/profile-cv/reprocess");
|
||||
await loadProfile();
|
||||
toast(t("profileCvReprocessed"), "success");
|
||||
toast(`Queued CV reprocess run (run ${res.data.extractionRunId}).`, "info");
|
||||
} catch (e: any) {
|
||||
toast(String(e?.response?.data || e?.message || t("profileCvReprocessFailed")), "error");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user