diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..f74ad6f --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,78 @@ +const BASE_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000"; + +async function apiFetch(path: string, token: string, options: RequestInit = {}) { + const res = await fetch(`${BASE_URL}${path}`, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...(options.headers ?? {}), + }, + }); + return res; +} + +export interface Preferences { + seniority: "INTERN" | "FULLTIME"; + locationPreferences: Array<"REMOTE" | "ONSITE" | "HYBRID">; +} + +export async function fetchPreferences(token: string): Promise { + const res = await apiFetch("/preferences", token); + if (res.status === 404) return null; + if (!res.ok) throw new Error("Failed to fetch preferences"); + return res.json(); +} + +export async function savePreferences(token: string, data: Preferences): Promise { + const res = await apiFetch("/preferences", token, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Failed to save preferences"); + return res.json(); +} + +export async function patchPreferences( + token: string, + data: Partial +): Promise { + const res = await apiFetch("/preferences", token, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Failed to update preferences"); + return res.json(); +} + +export interface ResumeData { + fileUrl: string; + uploadedAt: string; + parsedData?: Record | null; + message?: string; +} + +export async function fetchResume(token: string): Promise { + const res = await apiFetch("/resume", token); + if (res.status === 404) return null; + if (!res.ok) throw new Error("Failed to fetch resume"); + return res.json(); +} + +export async function uploadResume( + token: string, + file: File +): Promise<{ changed: boolean; fileUrl?: string; message: string }> { + const formData = new FormData(); + formData.append("file", file); + const res = await apiFetch("/resume", token, { + method: "POST", + body: formData, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error((body as { error?: string }).error ?? "Failed to upload resume"); + } + return res.json(); +} diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 6f3bf53..8905c61 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -1,31 +1,99 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useAuth } from "@clerk/react"; import { ResumeUpdateSection } from "../components/Profile/ResumeUpdateSection"; import { PreferencesForm } from "../components/Profile/PreferencesForm"; import { PreferencesSummary } from "../components/Profile/PreferencesSummary"; import type { Seniority, LocationType } from "../components/Profile/PreferencesForm"; +import { + fetchPreferences, + savePreferences, + fetchResume, + uploadResume, +} from "../lib/api"; export default function ProfilePage() { + const { getToken } = useAuth(); const [savedSeniority, setSavedSeniority] = useState("INTERN"); const [savedLocations, setSavedLocations] = useState(["REMOTE"]); - const [resumeFileName, setResumeFileName] = useState("resume_v1.pdf"); + const [resumeFileName, setResumeFileName] = useState(undefined); + const [resumeFileUrl, setResumeFileUrl] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isUploading, setIsUploading] = useState(false); + const [saveError, setSaveError] = useState(""); + const [uploadError, setUploadError] = useState(""); + + // Load existing preferences and resume on mount + useEffect(() => { + let cancelled = false; + async function load() { + try { + const token = await getToken(); + if (!token || cancelled) return; + + const [prefs, resume] = await Promise.all([ + fetchPreferences(token), + fetchResume(token), + ]); + + if (!cancelled) { + if (prefs) { + setSavedSeniority(prefs.seniority); + setSavedLocations(prefs.locationPreferences); + } + if (resume) { + const parsedUrl = new URL(resume.fileUrl); + const filename = parsedUrl.pathname.split("/").pop(); + setResumeFileName(filename ?? resume.fileUrl); + setResumeFileUrl(resume.fileUrl); + } + } + } catch (err) { + console.error("[ProfilePage] failed to load data:", err); + } + } + load(); + return () => { cancelled = true; }; + }, [getToken]); - const handlePreferencesSubmit = (seniority: Seniority, locations: LocationType[]) => { + const handlePreferencesSubmit = async (seniority: Seniority, locations: LocationType[]) => { setIsSaving(true); - setTimeout(() => { - setSavedSeniority(seniority); - setSavedLocations(locations); + setSaveError(""); + try { + const token = await getToken(); + if (!token) throw new Error("Not authenticated"); + const saved = await savePreferences(token, { + seniority, + locationPreferences: locations, + }); + setSavedSeniority(saved.seniority); + setSavedLocations(saved.locationPreferences); + } catch (err) { + console.error("[ProfilePage] failed to save preferences:", err); + setSaveError( + err instanceof Error ? err.message : "Failed to save preferences." + ); + } finally { setIsSaving(false); - }, 800); + } }; - const handleResumeReplace = (file: File) => { + const handleResumeReplace = async (file: File) => { setIsUploading(true); - setTimeout(() => { + setUploadError(""); + try { + const token = await getToken(); + if (!token) throw new Error("Not authenticated"); + const result = await uploadResume(token, file); setResumeFileName(file.name); + if (result.fileUrl) setResumeFileUrl(result.fileUrl); + } catch (err) { + console.error("[ProfilePage] failed to upload resume:", err); + setUploadError( + err instanceof Error ? err.message : "Failed to upload resume." + ); + } finally { setIsUploading(false); - }, 1000); + } }; return ( @@ -74,17 +142,26 @@ export default function ProfilePage() {
- + + {uploadError && ( +

{uploadError}

+ )} + - + + {saveError && ( +

{saveError}

+ )} + (null); const [extractedText, setExtractedText] = useState(""); const [status, setStatus] = useState("IDLE"); + const [uploadError, setUploadError] = useState(""); - const handleExtract = (f: File, text: string) => { + const handleExtract = async (f: File, text: string) => { setFile(f); setExtractedText(text); - setStatus("PARSED"); // was "DONE" — aligned to backend enum - console.log("Extracted text:", text); + setUploadError(""); + setStatus("PROCESSING"); + try { + const token = await getToken(); + if (!token) throw new Error("Not authenticated"); + await uploadResume(token, f); + setStatus("PARSED"); + } catch (err) { + console.error("[ResumePage] backend upload failed:", err); + setUploadError( + err instanceof Error ? err.message : "Upload to server failed." + ); + setStatus("FAILED"); + } }; const reset = () => { setFile(null); setExtractedText(""); setStatus("IDLE"); + setUploadError(""); }; return ( @@ -112,10 +129,21 @@ export function ResumePage() {
)} + {/* Upload error */} + {uploadError && ( +

{uploadError}

+ )} + {/* CTA */}

- Text extracted and ready for AI matching. + {status === "PARSED" + ? "Resume uploaded and queued for AI parsing." + : status === "PROCESSING" + ? "Uploading to server…" + : status === "FAILED" + ? "Upload failed. Please try again." + : "Text extracted and ready for AI matching."}