Commit 040b3b23 authored by Anthony Jacob's avatar Anthony Jacob
Browse files

add resume management

parent bc3a5e85
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
.resume-container button {
    margin-top: 10px;
}

#ResumeTab {
    justify-content: center;

}

input[type="file"] {
    margin-top: 10px;
}
 No newline at end of file
+157 −0
Original line number Diff line number Diff line
'use client';
import { useRef, useState } from "react";
import Tabs from "@/components/tabs";
import { removeResume, saveResume } from "./actions";
import './ClientResume.css';

export default function ClientResume({ data, enabledLanguage }: { data: any, enabledLanguage: any }) {

    const enabledLanguageList = (enabledLanguage?.languages || []).filter((l: any) => l.enabled);


    const [error, setError] = useState<string | null>(null);
    const [success, setSuccess] = useState(false);
    const [loading, setLoading] = useState(false);
    const fileBaseUrl = process.env.NEXT_PUBLIC_FILE_UPLOAD_DIR || "";
    // Helper to get resume file for a language
    const getResumeFile = (langCode: string) => {
        return (data.resumes || []).find((r: any) => r.language_code === langCode)?.file || null;
    };

    // Per-language file state
    const [resumeFiles, setResumeFiles] = useState<{ [lang: string]: File | null }>({});
    const [resumeIsUpdated, setResumeIsUpdated] = useState<{ [lang: string]: boolean }>({});
    const resumeInputRefs = useRef<{ [lang: string]: HTMLInputElement | null }>({});

    const handleResumeChange = (langCode: string, e: React.ChangeEvent<HTMLInputElement>) => {
        const file = e.target.files?.[0] || null;
        if (file) {
            if (file.type !== "application/pdf") {
                setError("Error: file must be a PDF");
                setResumeFiles(prev => ({ ...prev, [langCode]: null }));
                if (resumeInputRefs.current[langCode]) resumeInputRefs.current[langCode]!.value = "";
                return;
            }
            if (file.size > 10 * 1024 * 1024) { // 10MB
                setError("Error: file too large (max 10MB)");
                setResumeFiles(prev => ({ ...prev, [langCode]: null }));
                if (resumeInputRefs.current[langCode]) resumeInputRefs.current[langCode]!.value = "";
                return;
            }
        }
        setResumeFiles(prev => ({ ...prev, [langCode]: file }));
        setResumeIsUpdated(prev => ({ ...prev, [langCode]: true }));
        setError(null);
    };

    const handleDeleteResume = (langCode: string) => {
        setResumeFiles(prev => ({ ...prev, [langCode]: null }));
        if (resumeInputRefs.current[langCode]) resumeInputRefs.current[langCode]!.value = "";
        setResumeIsUpdated(prev => ({ ...prev, [langCode]: true }));
    }

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        setLoading(true);
        setError(null);
        setSuccess(false);

        try {
            // For each enabled language, handle upload or removal
            for (const lang of enabledLanguageList) {
                const langCode = lang.language_code;
                const file = resumeFiles[langCode];
                const isUpdated = resumeIsUpdated[langCode];

                if (isUpdated) {
                    if (file instanceof File) {
                        // Upload new/updated file
                        await saveResume(file, langCode);
                    } else if (file === null && getResumeFile(langCode)) {
                        // Remove file if it existed and now is removed
                        await removeResume(langCode);
                    }
                }
            }
            setSuccess(true);
        } catch (err: any) {
            setError(err.message || "Failed to update resumes");
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 resume-container">

            <h2 className="text-2xl font-semibold mb-4">Resume</h2>

            <form onSubmit={handleSubmit}>
                <Tabs Tabs={enabledLanguageList.map((t: any) => t.language_code)} Id="ResumeTab" />
                <div className="tab-content" id="ResumeTabContent">
                    {enabledLanguageList.map((lang: any, idx: number) => {
                        // Prefer state over original data
                        const fileFromState = resumeFiles[lang.language_code];
                        const isRemoved = resumeIsUpdated[lang.language_code] && fileFromState === null;
                        const resumeFile =
                            isRemoved
                                ? null
                                : fileFromState instanceof File
                                    ? fileFromState.name
                                    : (!resumeIsUpdated[lang.language_code] ? getResumeFile(lang.language_code) : null);

                        return (
                            <div
                                className={`tab-pane fade${idx === 0 ? " show active" : ""}`}
                                id={`${lang.language_code}-tab-pane`}
                                role="tabpanel"
                                aria-labelledby={`${lang.language_code}-tab`}
                                tabIndex={0}
                                key={lang.language_code}
                            >
                                <div className="mb-3">
                                    {resumeFile ? (
                                        <>
                                            <a
                                                href={`${fileBaseUrl}${resumeFile}`}
                                                target="_blank"
                                                rel="noopener noreferrer"
                                                className="btn btn-link"
                                            >
                                                {resumeFile}
                                            </a>
                                            <button
                                                type="button"
                                                className="btn btn-secondary btn-sm"
                                                onClick={() => handleDeleteResume(lang.language_code)}
                                            >
                                                Remove
                                            </button>
                                        </>
                                    ) : (
                                        <input
                                            className="form-control"
                                            type="file"
                                            id={`${lang.language_code}-resume-file`}
                                            accept="application/pdf"
                                            onChange={e => handleResumeChange(lang.language_code, e)}
                                            ref={el => { resumeInputRefs.current[lang.language_code] = el; }}
                                        />
                                    )}
                                </div>
                            </div>
                        );
                    })}
                </div>
                <div className="d-flex justify-content-end">
                    <button className="btn btn-primary" type="submit" disabled={loading}>
                        {loading ? "Saving..." : "Save"}
                    </button>
                </div>
                {error && <div className="alert alert-danger mt-2">{error}</div>}
                {success && <div className="alert alert-success mt-2">Saved!</div>}
            </form>

        </div>
    );
}
 No newline at end of file
+60 −0
Original line number Diff line number Diff line
"use server";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";

//YYYY-MM-DD
function formatDate(date: Date | null): string | null {
    if (!date) return null;
    return date.getFullYear() + '-' +
        String(date.getMonth() + 1).padStart(2, '0') + '-' +
        String(date.getDate()).padStart(2, '0');
}

export async function saveResume(file: File, langCode: string) {
    const cookieStore = cookies();
    const token = (await cookieStore).get("access_token")?.value;


    const formData = new FormData();
    formData.append("file", file);

    const res = await fetch(`${process.env.API_URL}/resumes/${langCode}`, {
        method: "POST",
        headers: {
            Authorization: `Bearer ${token}`,
        },
        body: formData,
    });

    if (!res.ok) {
        const errorText = await res.text();
        throw new Error(`Failed to upload resume: ${errorText}`);
    }
    revalidatePath("/Admin/Resume");
}

export async function removeResume(langCode: string) {
    const cookieStore = cookies();
    const token = (await cookieStore).get("access_token")?.value;


    const res = await fetch(`${process.env.API_URL}/resumes/${langCode}`, {
        method: "DELETE",
        headers: {
            Authorization: `Bearer ${token}`,
        },
    });

    console.log("Removing resume for Language:", langCode);

    if (!res.ok) {
        const errorText = await res.text();
        throw new Error(`Failed to delete resume: ${errorText}`);
    }
    else {
        console.log("resume removed successfully:", await res.text());
    }
    revalidatePath("/Admin/Resume");
}

+45 −0
Original line number Diff line number Diff line
import ClientResume from './ClientResume';
import { RefreshToken } from "@/components/refreshToken";

import { cookies } from "next/headers";
import { redirect } from 'next/navigation';
import fs from 'fs/promises';
import path from 'path';

export default async function Resume() {
  const cookieStore = cookies();
  const token = (await cookieStore).get("access_token")?.value;

  const res = await fetch(`${process.env.API_URL}/resumes/full`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    cache: 'no-store',
  });


  const jsonbody = await res.json();
  const data = res.ok ? jsonbody : null;

  const resLanguage = await fetch(`${process.env.API_URL}/languages/full`, {
    cache: 'no-store',
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  const enabledLanguage = resLanguage.ok ? await resLanguage.json() : null;

  if (jsonbody?.msg === "Token has been revoked" || enabledLanguage?.msg === "Token has been revoked") {
    //redirect to logout
    return redirect("/logout");
  }



  return <>
    <RefreshToken />
    <ClientResume data={data} enabledLanguage={enabledLanguage} />

  </>;
}
 No newline at end of file