diff --git a/backend/controllers/resume/resume-controller.js b/backend/controllers/resume/resume-controller.js index 1a90e17..64fb986 100644 --- a/backend/controllers/resume/resume-controller.js +++ b/backend/controllers/resume/resume-controller.js @@ -6,7 +6,8 @@ const putSection = async (req, res, logger) => { logger.info(`PUT /resume/${section_name}`); const { section } = req.body; - console.log(section); + + logger.debug(section); const resume = await resumeUtils.getSingletonResume(); const id = resume._id; const updatedResume = await resumeDao.updateSectionById( diff --git a/frontend/src/components/LeftPanel/LeftPanel.js b/frontend/src/components/LeftPanel/LeftPanel.js index 8498544..efa5515 100644 --- a/frontend/src/components/LeftPanel/LeftPanel.js +++ b/frontend/src/components/LeftPanel/LeftPanel.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import NavBar from "./NavBar"; import Header from "./Header"; import Basics from "./sections/Basics"; @@ -16,8 +16,12 @@ import { volunteerSectionConfig, workExperienceSectionConfig, } from "../../config/sectionConfig"; +import { useDispatch, useSelector } from "react-redux"; +import { putSectionThunk } from "../../services/resume-thunk"; const LeftPanel = () => { + let { resume, resumeLoading } = useSelector((state) => state.resume); + const sectionsList = [ { type: "Basics", @@ -69,6 +73,35 @@ const LeftPanel = () => { }, ]; + const dispatch = useDispatch(); + + useEffect(() => { + if (!resumeLoading && resume !== null) { + const interval = setInterval(() => { + let resumeLocalStorage = localStorage.getItem("resume"); + if (resumeLocalStorage !== JSON.stringify(resume)) { + const resumeLocalStorageObject = JSON.parse(resumeLocalStorage); + + Object.keys(resumeLocalStorageObject).map((key) => { + if ( + JSON.stringify(resumeLocalStorageObject[key]) !== + JSON.stringify(resume[key]) + ) { + let section_name = key; + let section = {}; + section[key] = resume[key]; + dispatch(putSectionThunk({ section_name, section })); + } + + return null; + }); + } + }, 3000); + + return () => clearInterval(interval); + } + }, [resume, resumeLoading]); + return (
diff --git a/frontend/src/components/LeftPanel/sections/Basics.js b/frontend/src/components/LeftPanel/sections/Basics.js index 8b90283..ae6cd7b 100644 --- a/frontend/src/components/LeftPanel/sections/Basics.js +++ b/frontend/src/components/LeftPanel/sections/Basics.js @@ -12,6 +12,10 @@ import { getResumeThunk, putSectionThunk, } from "../../../services/resume-thunk"; +import { + getCurrentResume, + updateResume, +} from "../../../reducers/resume-reducer"; const Basics = () => { const { imageUploading, imageURL } = useSelector( @@ -26,17 +30,15 @@ const Basics = () => { dispatch(uploadImageThunk(e.target.files[0])); }; - const updateInLocalStorage = (key, value) => { - if (!resumeLoading && resume !== null) { - let resume = JSON.parse(localStorage.getItem("resume")); - resume.basics[key] = value; - localStorage.setItem("resume", JSON.stringify(resume)); - } - }; - const onTextFieldKeyUp = (e) => { setBasicsObj({ ...basicsObj, [e.target.id]: e.target.value }); - updateInLocalStorage(e.target.id, e.target.value); + dispatch( + updateResume({ + sectionKeys: ["basics"], + key: e.target.id, + value: e.target.value, + }) + ); }; useEffect(() => { @@ -47,28 +49,6 @@ const Basics = () => { useEffect(() => { if (!resumeLoading && resume !== null) { setBasicsObj(resume.basics); - const interval = setInterval(() => { - let resumeLocalStorage = localStorage.getItem("resume"); - if (resumeLocalStorage !== JSON.stringify(resume)) { - const resumeLocalStorageObject = JSON.parse(resumeLocalStorage); - - Object.keys(resumeLocalStorageObject).map((key) => { - if ( - JSON.stringify(resumeLocalStorageObject[key]) !== - JSON.stringify(resume[key]) - ) { - let section_name = key; - let section = {}; - section[key] = resumeLocalStorageObject[key]; - dispatch(putSectionThunk({ section_name, section })); - } - - return null; - }); - } - }, 10000); - - return () => clearInterval(interval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [resume, resumeLoading]); diff --git a/frontend/src/components/LeftPanel/sections/Location.js b/frontend/src/components/LeftPanel/sections/Location.js index 0346524..76e0642 100644 --- a/frontend/src/components/LeftPanel/sections/Location.js +++ b/frontend/src/components/LeftPanel/sections/Location.js @@ -1,29 +1,30 @@ import { Apartment } from "@mui/icons-material"; import { TextField } from "@mui/material"; import React, { useEffect, useState } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; +import { updateResume } from "../../../reducers/resume-reducer"; const Location = () => { const { resume, resumeLoading } = useSelector((state) => state.resume); let [locationObj, setLocationObj] = useState({}); + const dispatch = useDispatch(); + useEffect(() => { if (!resumeLoading && resume !== null) { setLocationObj(resume.basics.location); } }, [resume, resumeLoading]); - const updateInLocalStorage = (key, value) => { - if (!resumeLoading && resume !== null) { - let resume = JSON.parse(localStorage.getItem("resume")); - resume.basics.location[key] = value; - localStorage.setItem("resume", JSON.stringify(resume)); - } - }; - const onTextFieldKeyUp = (e) => { setLocationObj({ ...locationObj, [e.target.id]: e.target.value }); - updateInLocalStorage(e.target.id, e.target.value); + dispatch( + updateResume({ + sectionKeys: ["basics", "location"], + key: e.target.id, + value: e.target.value, + }) + ); }; return ( diff --git a/frontend/src/components/LeftPanel/sections/modals/GenericListModal.js b/frontend/src/components/LeftPanel/sections/modals/GenericListModal.js index 2cb970e..3e66e6d 100644 --- a/frontend/src/components/LeftPanel/sections/modals/GenericListModal.js +++ b/frontend/src/components/LeftPanel/sections/modals/GenericListModal.js @@ -12,8 +12,12 @@ import { import { DatePicker } from "@mui/x-date-pickers"; import dayjs from "dayjs"; import React, { useState, useEffect } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { modes } from "../../constants/modes"; +import { + updateResume, + updateResumeArray, +} from "../../../../reducers/resume-reducer"; const GenericModal = ({ fieldsMap, @@ -35,6 +39,18 @@ const GenericModal = ({ let [genericFieldEntryIdxMap, setGenericEntryFieldIdxMap] = useState({}); let [isEditingFieldEntryIdxMap, setIsEditingFieldEntryIdxMap] = useState({}); + const dispatch = useDispatch(); + + const cleanState = () => { + setModalEntryObject({}); + setGenericListMap({}); + setGenericEntryMap({}); + setGenericEntryFieldIdxMap({}); + setIsEditingFieldEntryIdxMap({}); + setOpenModal(false); + setModalMode(modes.ADD); + }; + // To store the markup for all fields let fieldDOM = {}; @@ -121,11 +137,27 @@ const GenericModal = ({ setEntryList([...entryList, { ...modalEntryObject, ...genericListMap }]); } - setOpenModal(false); - setModalMode(modes.ADD); const finalMap = { ...modalEntryObject, ...genericListMap }; - updateInLocalStorage(finalMap); - setGenericListMap({}); + + if (modalMode == modes.EDIT) { + console.log(dbField, currentModalIdx); + dispatch( + updateResume({ + sectionKeys: dbField, + key: currentModalIdx, + value: finalMap, + }) + ); + } else { + dispatch( + updateResumeArray({ + sectionKeys: dbField, + value: finalMap, + }) + ); + } + + cleanState(); }; const getValue = (fieldName) => { @@ -320,15 +352,7 @@ const GenericModal = ({ return ( <> - { - setOpenModal(false); - setModalMode(modes.ADD); - setIsEditingFieldEntryIdxMap({}); - setGenericEntryFieldIdxMap({}); - }} - > + Add a {fieldName} {fieldGroups.map((group, idx) => { diff --git a/frontend/src/components/RightPanel/RightPanel.js b/frontend/src/components/RightPanel/RightPanel.js index a2a943a..b016222 100644 --- a/frontend/src/components/RightPanel/RightPanel.js +++ b/frontend/src/components/RightPanel/RightPanel.js @@ -1,11 +1,8 @@ import React from "react"; +import Resume from "./resumes/template-1/Resume"; const RightPanel = () => { - return ( -
- Resume Render -
- ); + return ; }; export default RightPanel; diff --git a/frontend/src/components/RightPanel/resumes/template-1/Resume.css b/frontend/src/components/RightPanel/resumes/template-1/Resume.css new file mode 100644 index 0000000..040b246 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/Resume.css @@ -0,0 +1,48 @@ +.resume { + text-align: center; + padding-left: 50px; + padding-right: 50px; + } + + .left { + text-align: left; + } + + .no-margin-bottom { + margin-bottom: 0; + } + + .no-margin-top { + margin-top: -10px; + } + + .float-left { + float: left; + } + + .float-right { + float: right; + } + + .padding-left { + padding-left: 20px; + } + + .clear { + clear: both; + } + + .green { + color: hsl(159, 83%, 28%); + } + + hr { + border: none; + height: 2px; + background-color: black; /* Modern Browsers */ + } + + tr { + text-align: left; + } + \ No newline at end of file diff --git a/frontend/src/components/RightPanel/resumes/template-1/Resume.js b/frontend/src/components/RightPanel/resumes/template-1/Resume.js new file mode 100644 index 0000000..ce26089 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/Resume.js @@ -0,0 +1,166 @@ +import { useSelector } from "react-redux"; +import "./Resume.css"; +import PrimarySecondary from "./components/primary-secondary/PrimarySecondary"; +import Section from "./components/section/Section"; +import YearHighlights from "./components/year-highlights/YearHighlights"; +import * as objectUtils from "./utils/object_utils"; +import { useEffect, useState } from "react"; + +function Resume() { + let { resume, resumeLoading } = useSelector((state) => state.resume); + + const getKeywordAsString = (keywords) => { + let keywordString = ""; + keywords.forEach((keyword, idx) => { + if (idx === keywords.length - 1) { + keywordString += keyword; + } else { + keywordString += keyword + ", "; + } + }); + return keywordString; + }; + + const getSecondaryInfoEducation = (obj) => { + const location = objectUtils.getKeyOrEmptyString(obj, ["location"]); + return `${location}`; + }; + + const getSecondaryInfoWork = (obj) => { + const position = objectUtils.getKeyOrEmptyString(obj, ["position"]); + const location = objectUtils.getKeyOrEmptyString(obj, ["location"]); + return `${position}, ${location}`; + }; + + const getSecondaryInfoProject = (obj) => { + const url = objectUtils.getKeyOrEmptyString(obj, ["url"]); + return `[${url}]`; + }; + + return ( + <> +
+

{objectUtils.getKeyOrEmptyString(resume, ["basics", "name"])}

+

+ {objectUtils.getKeyOrEmptyString(resume, ["basics", "email"])} |{" "} + {objectUtils.getKeyOrEmptyString(resume, [ + "basics", + "profiles", + 0, + "url", + ])}{" "} + |{" "} + {objectUtils.getKeyOrEmptyString(resume, [ + "basics", + "profiles", + 1, + "url", + ])}{" "} + |{" "} + {objectUtils.getKeyOrEmptyString(resume, [ + "basics", + "location", + "city", + ])} + ,{" "} + {objectUtils.getKeyOrEmptyString(resume, [ + "basics", + "location", + "region", + ])}{" "} + | {objectUtils.getKeyOrEmptyString(resume, ["basics", "phone"])} +

+
+ + {objectUtils + .getKeyOrEmptyArray(resume, ["education"]) + .map((edu, idx) => { + return ( +
+ { + return val; + }} + getSecondaryInfo={getSecondaryInfoEducation} + primaryField="institution" + addLineBreaks={false} + /> +

+ + {objectUtils.getKeyOrEmptyString(edu, ["studyType"])} in{" "} + {objectUtils.getKeyOrEmptyString(edu, ["area"])}, GPA:{" "} + + {objectUtils.getKeyOrEmptyString(edu, ["score"])} + + +

+

+ Related courses:{" "} + {objectUtils + .getKeyOrEmptyArray(edu, ["courses"]) + .map((course, idx) => { + if ( + idx === + objectUtils.getKeyOrEmptyArray(edu, ["courses"]) + .length - + 1 + ) { + return course; + } else { + return course + ", "; + } + })} +

+
+ ); + })} + +
+ + + + {objectUtils + .getKeyOrEmptyArray(resume, ["skills"]) + .map((skill, idx) => { + return ( + + + + + ); + })} + +
+ + {objectUtils.getKeyOrEmptyString(skill, ["name"])}: + + + {getKeywordAsString( + objectUtils.getKeyOrEmptyArray(skill, ["keywords"]) + )} +
+ + { + return val.toUpperCase(); + }} + /> + + { + return val; + }} + /> +
+ + ); +} + +export default Resume; diff --git a/frontend/src/components/RightPanel/resumes/template-1/components/primary-secondary/PrimarySecondary.js b/frontend/src/components/RightPanel/resumes/template-1/components/primary-secondary/PrimarySecondary.js new file mode 100644 index 0000000..609a217 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/components/primary-secondary/PrimarySecondary.js @@ -0,0 +1,39 @@ +import React from "react"; + +import "../../Resume.css"; +import * as dateUtils from "../../utils/date_utils"; +import * as objectUtils from "../../utils/object_utils"; + +const PrimarySecondary = ({ + obj, + getPrimaryInfo, + getSecondaryInfo, + primaryField = "name", + addLineBreaks = true, +}) => { + return ( + <> +

+ + + {getPrimaryInfo( + objectUtils.getKeyOrEmptyString(obj, [primaryField]) + )} + {" "} + {getSecondaryInfo(obj)} + + + {dateUtils.getStartEndDateString(obj)} + +

+ {addLineBreaks && ( + <> +
+
+ + )} + + ); +}; + +export default PrimarySecondary; diff --git a/frontend/src/components/RightPanel/resumes/template-1/components/section/Section.js b/frontend/src/components/RightPanel/resumes/template-1/components/section/Section.js new file mode 100644 index 0000000..e9944d3 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/components/section/Section.js @@ -0,0 +1,16 @@ +import React from "react"; + +import "../../Resume.css"; + +const Section = ({ title }) => { + return ( + <> +

+ {title.toUpperCase()} +

+
+ + ); +}; + +export default Section; diff --git a/frontend/src/components/RightPanel/resumes/template-1/components/year-highlights/YearHighlights.js b/frontend/src/components/RightPanel/resumes/template-1/components/year-highlights/YearHighlights.js new file mode 100644 index 0000000..d0f1f4a --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/components/year-highlights/YearHighlights.js @@ -0,0 +1,44 @@ +import React from "react"; + +import "../../Resume.css"; +import Section from "../section/Section"; +import PrimarySecondary from "../primary-secondary/PrimarySecondary"; +import * as objectUtils from "../../utils/object_utils"; + +const YearHighlights = ({ + arrayObj, + sectionTitle, + getPrimaryInfo, + getSecondaryInfo, +}) => { + return ( + <> +
+ {arrayObj.map((obj, idx) => { + return ( +
+ +
    + {objectUtils + .getKeyOrEmptyArray(obj, ["highlights"]) + .map((highlight, idx) => { + return ( +
  • + ); + })} +
+
+ ); + })} + + ); +}; + +export default YearHighlights; diff --git a/frontend/src/components/RightPanel/resumes/template-1/utils/date_utils.js b/frontend/src/components/RightPanel/resumes/template-1/utils/date_utils.js new file mode 100644 index 0000000..44de6c0 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/utils/date_utils.js @@ -0,0 +1,48 @@ +import * as objectUtils from "./object_utils"; + +const convertMonthToName = (month) => { + const monthToName = { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }; + + return monthToName[month]; +}; + +export const getStartEndDateString = (obj) => { + let dateString = ""; + + if ("startDate" in obj) { + dateString += + convertMonthToName( + objectUtils.getKeyOrEmptyString(obj, ["startDate", "month"]) + ) + + " " + + objectUtils.getKeyOrEmptyString(obj, ["startDate", "year"]); + } + + dateString += " - "; + + if ("endDate" in obj) { + dateString += + convertMonthToName( + objectUtils.getKeyOrEmptyString(obj, ["endDate", "month"]) + ) + + " " + + objectUtils.getKeyOrEmptyString(obj, ["endDate", "year"]); + } else { + dateString += "Present"; + } + + return dateString; +}; diff --git a/frontend/src/components/RightPanel/resumes/template-1/utils/object_utils.js b/frontend/src/components/RightPanel/resumes/template-1/utils/object_utils.js new file mode 100644 index 0000000..4d96071 --- /dev/null +++ b/frontend/src/components/RightPanel/resumes/template-1/utils/object_utils.js @@ -0,0 +1,31 @@ +export const getKeyOrEmptyAny = (obj, key, any) => { + if (obj == null) { + return any; + } + + let finalValue = any; + if (!(key[0] in obj)) { + return finalValue; + } + finalValue = obj[key[0]]; + for (let i = 1; i < key.length; i++) { + if (finalValue == null) { + return any; + } + + if (!(key[i] in finalValue)) { + return any; + } + finalValue = finalValue[key[i]]; + } + + return finalValue; +}; + +export const getKeyOrEmptyString = (obj, key) => { + return getKeyOrEmptyAny(obj, key, ""); +}; + +export const getKeyOrEmptyArray = (obj, key) => { + return getKeyOrEmptyAny(obj, key, []); +}; diff --git a/frontend/src/reducers/resume-reducer.js b/frontend/src/reducers/resume-reducer.js index fbd5cb7..92d7371 100644 --- a/frontend/src/reducers/resume-reducer.js +++ b/frontend/src/reducers/resume-reducer.js @@ -10,7 +10,29 @@ let initialState = { const resumeSlice = createSlice({ name: "resume", initialState, - reducers: {}, + reducers: { + updateResume: (state, action) => { + const sectionKeys = action.payload.sectionKeys; + const key = action.payload.key; + const value = action.payload.value; + + let resume = state.resume[sectionKeys[0]]; + for (let i = 1; i < sectionKeys.length; i++) { + resume = resume[sectionKeys[i]]; + } + resume[key] = value; + }, + updateResumeArray: (state, action) => { + const sectionKeys = action.payload.sectionKeys; + const value = action.payload.value; + + let resume = state.resume[sectionKeys[0]]; + for (let i = 1; i < sectionKeys.length; i++) { + resume = resume[sectionKeys[i]]; + } + resume.push(value); + }, + }, extraReducers: { [getResumeThunk.pending]: (state, _action) => { state.resumeLoading = true; @@ -43,4 +65,5 @@ const resumeSlice = createSlice({ }, }); +export const { updateResume, updateResumeArray } = resumeSlice.actions; export default resumeSlice.reducer;