Changeset 4d97b63 for jobvista-frontend/src
- Timestamp:
- 08/30/24 15:44:27 (3 months ago)
- Branches:
- main
- Parents:
- 0f0add0
- Location:
- jobvista-frontend/src
- Files:
-
- 17 edited
Legend:
- Unmodified
- Added
- Removed
-
jobvista-frontend/src/App.css
r0f0add0 r4d97b63 38 38 39 39 .form-container { 40 font-family: "Segoe UI"; 40 41 background-color: white; 41 42 border-radius: 10px; 42 43 padding: 15px 30px; 43 margin-top: 80px; 44 margin-top: 40px; 45 } 46 47 48 .form-container h5, .form-container h3 { 49 text-align:center; 50 font-family: Poppins 44 51 } 45 52 … … 217 224 } 218 225 219 220 221 222 226 .card-company-logo { 227 border-radius: 15% 228 } 229 230 231 232 233 -
jobvista-frontend/src/redux/actionTypes.js
r0f0add0 r4d97b63 13 13 export const FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER = "FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER" 14 14 export const SUBMIT_APPLICATION = "SUBMIT_APPLICATION" 15 export const UPDATE_APPLICATION = "UPDATE_APPLICATION" 15 16 export const UPDATE_APPLICATION_STATUS = "UPDATE_APPLICATION_STATUS" 17 export const UPDATE_APPLICATIONS = "UPDATE_APPLICATIONS" 16 18 export const FETCH_APPLICATIONS_BY_JOB_ID = "FETCH_APPLICATIONS_BY_JOB_ID" 19 export const FILTER_APPLICATIONS_BY_JOB_ID = "FILTER_APPLICATIONS_BY_JOB_ID" 17 20 export const FETCH_APPLICATIONS_BY_JOB_SEEKER_ID = "FETCH_APPLICATIONS_BY_JOB_SEEKER_ID" 21 export const FILTER_APPLICATIONS_BY_JOB_SEEKER_ID = "FILTER_APPLICATIONS_BY_JOB_SEEKER_ID" 18 22 export const DOWNLOAD_RESUME = "DOWNLOAD_RESUME" 19 23 -
jobvista-frontend/src/redux/actions/applicationActions.js
r0f0add0 r4d97b63 3 3 CURRENT_USER, 4 4 FETCH_APPLICATIONS_BY_JOB_ID, 5 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 6 SUBMIT_APPLICATION, UPDATE_APPLICATION _STATUS5 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 6 SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS, UPDATE_APPLICATIONS 7 7 } from "../actionTypes"; 8 8 … … 18 18 dispatch({ 19 19 type: SUBMIT_APPLICATION, 20 application: response.data 21 }) 22 callback(true, response) 23 }).catch(error => { 24 callback(false, error) 25 console.log(error) 26 }) 27 } 28 }, 29 30 updateApplication: (applicationId, additionalFiles, callback) => { 31 console.log(additionalFiles) 32 return dispatch => { 33 axios.post("/applications/"+ applicationId + "/update", additionalFiles, { 34 headers: { 35 'Content-Type': 'multipart/form-data' 36 } 37 }) 38 .then(response => { 39 dispatch({ 40 type: UPDATE_APPLICATION, 20 41 application: response.data 21 42 }) … … 45 66 } 46 67 }, 68 69 updateApplications: (changes, callback) => { 70 return dispatch => { 71 axios.post("/applications/update", changes) 72 .then(response => { 73 dispatch({ 74 type: UPDATE_APPLICATIONS, 75 applications: response.data 76 }) 77 callback(true, response) 78 }).catch(error => { 79 callback(false, error) 80 }) 81 } 82 }, 83 47 84 fetchApplicationsByJobSeeker: (jobSeekerId, callback) => { 48 85 return dispatch => { … … 51 88 dispatch({ 52 89 type: FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 90 applicationsByJobSeeker: response.data 91 }) 92 callback(true, response) 93 }).catch(error => { 94 callback(false, error) 95 }) 96 } 97 }, 98 99 filterApplicationsByJobSeeker: (jobSeekerId, status, callback) => { 100 return dispatch => { 101 axios.post("/my-applications/" + jobSeekerId + "/filtered", status) 102 .then(response => { 103 dispatch({ 104 type: FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 53 105 applicationsByJobSeeker: response.data 54 106 }) … … 75 127 } 76 128 }, 129 130 filterApplicationsByJobAdId: (jobAdId, status, callback) => { 131 return dispatch => { 132 axios.post("/job-advertisements/" + jobAdId + "/applications/filtered", status) 133 .then(response => { 134 dispatch({ 135 type: FILTER_APPLICATIONS_BY_JOB_ID, 136 applicationsByJobAdId: response.data 137 }) 138 callback(true, response) 139 } 140 ).catch(error => { 141 callback(false, error) 142 }) 143 } 144 }, 77 145 downloadResume: (id, callback) => { 78 146 return axios.get("/applications/" + id + "/download-resume", {responseType: "blob"}) … … 86 154 }) 87 155 156 }, 157 downloadAdditionalFiles: (id, callback) => { 158 return axios.get("/applications/" + id + "/download-additional-files") 159 .then(response => { 160 const urls = response.data; // This will be a list of URLs 161 callback(true, urls); 162 }) 163 .catch(error => { 164 callback(false, error); 165 }); 166 88 167 } 89 168 } -
jobvista-frontend/src/redux/actions/authActions.js
r0f0add0 r4d97b63 32 32 }).catch((error) => { 33 33 callback(false, error); 34 }); 35 }; 36 }, 37 38 signInGoogle: (tokenId, callback) => { 39 return dispatch => { 40 axios.post("/auth/google", { tokenId }) 41 .then(jwtResponse => { 42 const response = jwtResponse.data; 43 const token = response.token; 44 const user = { 45 name: response.name, 46 role: response.role, 47 access: response.hasAccess, 48 id: response.id, 49 }; 50 dispatch({ 51 type: SIGN_IN, 52 payload: { 53 token, 54 user 55 } 56 }); 57 callback && callback(true); 58 }).catch(error => { 59 callback && callback(false, error); 34 60 }); 35 61 }; -
jobvista-frontend/src/redux/reducers/applicationReducer.js
r0f0add0 r4d97b63 2 2 CURRENT_USER, 3 3 FETCH_APPLICATIONS_BY_JOB_ID, 4 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 5 SUBMIT_APPLICATION, UPDATE_APPLICATION _STATUS4 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 5 SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS 6 6 } from "../actionTypes"; 7 7 … … 22 22 applicationsByJobSeeker: [...state.applicationsByJobSeeker, action.application] 23 23 } 24 case UPDATE_APPLICATION: 25 return { 26 ...state, 27 applicationsByJobSeeker: state.applicationsByJobSeeker.map(application => 28 application.id === action.application.id ? 29 action.application : // Replace with the updated application 30 application // Keep the old one 31 ) 32 } 24 33 case UPDATE_APPLICATION_STATUS: 25 34 return { … … 36 45 applicationsByJobAdId: action.applicationsByJobAdId 37 46 } 47 case FILTER_APPLICATIONS_BY_JOB_ID: 48 return { 49 ...state, 50 applicationsByJobAdId: action.applicationsByJobAdId 51 } 52 38 53 case FETCH_APPLICATIONS_BY_JOB_SEEKER_ID: 54 return { 55 ...state, 56 applicationsByJobSeeker: action.applicationsByJobSeeker 57 } 58 case FILTER_APPLICATIONS_BY_JOB_SEEKER_ID: 39 59 return { 40 60 ...state, -
jobvista-frontend/src/utils/toastUtils.js
r0f0add0 r4d97b63 108 108 toast.success( 109 109 <span> 110 Status updated successfully!110 Application/s updated successfully! 111 111 </span>, { 112 112 position: "bottom-right", … … 126 126 <span> 127 127 Your application was successfully sent! 128 </span>, { 129 position: "bottom-right", 130 autoClose: 5000, 131 hideProgressBar: false, 132 closeOnClick: true, 133 pauseOnHover: false, 134 draggable: true, 135 progress: undefined, 136 theme: "dark", 137 pauseOnFocusLoss: false 138 }); 139 } 140 141 export const notifyJobAdUpdate= () => { 142 toast.success( 143 <span> 144 Your application was successfully updated! 128 145 </span>, { 129 146 position: "bottom-right", -
jobvista-frontend/src/views/applications/ApplicationDetailsModal.js
r0f0add0 r4d97b63 17 17 import Roles from "../../enumerations/Roles"; 18 18 import {ApplicationActions} from "../../redux/actions/applicationActions"; 19 import {notifyJobAdApply, notifyJobAdUpdate} from "../../utils/toastUtils"; 19 20 20 21 … … 25 26 const auth = useSelector(state => state.auth.currentUser) 26 27 const [resumeUrl, setResumeUrl] = useState(""); 28 const [additionalFileUrls, setAdditionalFileUrls] = useState([]); 27 29 28 //const [resumeFile, setResumeFile] = useState(null);30 const [additionalFiles, setAdditionalFiles] = useState(null); 29 31 const toggleModal = () => { 30 32 setModal(!modal); 31 33 }; 34 35 const {register, handleSubmit, control, formState: {errors}} = useForm(); 32 36 33 37 useEffect(() => { … … 36 40 if (success) { 37 41 setResumeUrl(response); 42 43 if (application.additionalFileNames.length > 0) { 44 ApplicationActions.downloadAdditionalFiles(application.id, (success2, response) => { 45 if (success2) { 46 setAdditionalFileUrls(response); 47 } 48 }) 49 } 38 50 } 39 51 }) 40 52 } 41 53 }, []) 54 55 const updateApplication = async () => { 56 try { 57 const formData = new FormData(); 58 if (additionalFiles && additionalFiles.length > 0) { 59 for (let i = 0; i < additionalFiles.length; i++) { 60 formData.append('additionalFiles', additionalFiles[i]); 61 } 62 } 63 64 dispatch(ApplicationActions.updateApplication(application.id, formData, (success) => { 65 if (success) { 66 toggleModal() 67 window.location.reload() 68 } 69 })) 70 } catch (err) { 71 console.error(err) 72 } 73 } 42 74 43 75 function getFileName(path) { … … 50 82 51 83 return (<div className="modal-wrap"> 52 <button onClick={toggleModal} className="application-button">View application</button> 84 {auth.role === Roles.RECRUITER ? <button onClick={toggleModal} className="application-button">View 85 application</button> : (application.status === "UNDER_REVIEW" && application.response.length > 0 && additionalFileUrls.length === 0) ? 86 <button onClick={toggleModal} className="application-button">Update application</button> : 87 <button onClick={toggleModal} className="application-button">View application</button>} 88 53 89 <Modal open={modal} onClose={toggleModal} center> 54 90 <div className="head-modal"> … … 58 94 59 95 <div className="modal-content"> 60 <form >96 <form onSubmit={handleSubmit(updateApplication)}> 61 97 <div className="row"> 62 <div className="col-md-6"> 63 <label className="label">Why are you interested in joining our company?</label> 64 <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled 65 placeholder="Write your answer here..." className="application-textarea"/> 66 <br/><br/> 67 <label className="label">What makes you a good fit for this position?</label> 68 <textarea disabled type="text" defaultValue={application.questionAnswers[1]} 69 placeholder="Write your answer here..." className="application-textarea"/> 70 <br/><br/> 71 <label className="label">What do you hope to achieve in your first 6 months in this 72 role?</label> 73 <textarea disabled type="text" defaultValue={application.questionAnswers[2]} 74 placeholder="Write your answer here..." className="application-textarea"/> 98 <div className="col-md-6 d-flex flex-column gap-4"> 99 <div> 100 <label className="label">Why are you interested in joining our company?</label> 101 <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled 102 placeholder="Write your answer here..." className="application-textarea"/> 103 </div> 104 105 <div> 106 <label className="label">What makes you a good fit for this position?</label> 107 <textarea disabled type="text" defaultValue={application.questionAnswers[1]} 108 placeholder="Write your answer here..." className="application-textarea"/> 109 </div> 110 111 <div> 112 <label className="label">What do you hope to achieve in your first 6 months in this 113 role?</label> 114 <textarea disabled type="text" defaultValue={application.questionAnswers[2]} 115 placeholder="Write your answer here..." className="application-textarea"/> 116 </div> 117 75 118 76 119 </div> 77 <div className="col-md-6"> 78 <label htmlFor="start">Curriculum vitae (CV)</label> 79 <br/> 80 <a className="resume-link" href={resumeUrl} target="_blank" 81 rel="noopener noreferrer">{getFileName(application.fileName)}</a> 82 <br/> 120 <div className="col-md-6 d-flex flex-column gap-4"> 121 <div> 122 <label className="label" htmlFor="start">Curriculum vitae (CV)</label> 83 123 84 <br/> 85 <label className="label">Message to the recruiter</label> 86 <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." 87 className="application-textarea"/> 124 <a className="resume-link" href={resumeUrl} target="_blank" 125 rel="noopener noreferrer">{getFileName(application.fileName)}</a> 126 </div> 127 128 <div> 129 <label className="label">Message to the recruiter</label> 130 <textarea disabled type="text" defaultValue={application.message} 131 placeholder="Optional..." 132 className="application-textarea"/> 133 </div> 134 135 136 {additionalFileUrls.length > 0 ? (<div> 137 <label className="label" htmlFor="start">Additional documents</label> 138 <ul style={{listStyleType: "none", padding: 0, margin: 0}}> 139 {additionalFileUrls.map((url, index) => ( 140 <li style={{marginBottom: 10}} key={index}> 141 <a href={url} className="resume-link" download target="_blank"> 142 {getFileName(url)} 143 </a> 144 </li>))} 145 </ul> 146 </div>) : (<div> 147 {(application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) && 148 <div> 149 <label className="label" htmlFor="start">Additional documents</label> 150 <input 151 className="resume-link" 152 onChange={(e) => setAdditionalFiles(e.target.files)} 153 required type="file" 154 id="fileUpload" 155 accept=".pdf" 156 multiple 157 /> 158 </div>} 159 </div> 160 )} 161 88 162 89 163 </div> 90 164 </div> 165 {(additionalFileUrls.length === 0 && application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) && 166 <div className="modal-buttons"> 167 <div className="cancel-btn" onClick={toggleModal}> Cancel</div> 168 <button className="submit-btn"> Submit</button> 169 </div>} 91 170 92 171 </form> 172 173 93 174 </div> 94 175 </Modal> -
jobvista-frontend/src/views/applications/Applications.css
r0f0add0 r4d97b63 17 17 } 18 18 19 /*.application-title span {*/ 20 /* font-weight: bold;*/ 21 /* */ 22 /*}*/ 19 .response-message { 20 background-color: floralwhite; 21 border-radius: 8px; 22 padding: 15px; 23 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 24 } 25 26 27 28 29 .application-card-wrapper { 30 margin: 15px 0; 31 display: flex; 32 flex-direction: column; 33 transition: all 0.4s ease-in-out; 34 gap: 3px; 35 } 36 37 .application-card.changed { 38 background-color: aliceblue; 39 } 40 41 .application-card-wrapper .expand-section { 42 max-height: 0; 43 opacity: 0; 44 transition: 0.5s ease; 45 display: flex; 46 flex-direction: column; 47 align-items: flex-end; 48 margin: 0 !important; 49 } 50 51 .application-card-wrapper.expanded .expand-section { 52 max-height: 200px; 53 opacity: 1; 54 transform: translateY(0); 55 margin-top: 10px; 56 /* transition: max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;*/ 57 } 58 59 .expand-section textarea { 60 width: 100%; 61 padding: 10px; 62 border-radius: 8px; 63 border: 1px solid #ccc; 64 } 65 23 66 24 67 .application-card { … … 29 72 transition: all 0.3s ease; 30 73 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 31 height: auto;74 /*height: auto;*/ 32 75 padding: 20px 20px; 33 76 display: flex; 34 /*justify-content: center;*/35 /*gap: 20px;*/36 margin: 15px 0;37 /*z-index: -1000;*/38 77 } 39 78 .application-card .app-job-seeker-pic { … … 51 90 52 91 .application-card .app-info { 53 width: 6 5%;92 width: 60%; 54 93 display: inline-flex; 55 94 flex-direction: column; … … 86 125 87 126 .application-card .app-status { 88 width: 28%;127 width: 38%; 89 128 display: inline-flex; 90 129 justify-content: end; … … 98 137 } 99 138 100 . status {139 .app-status .status { 101 140 color: white; 102 141 padding: 5px 10px; … … 105 144 text-align: center; 106 145 } 146 147 148 149 150 151 107 152 108 153 … … 168 213 transform: rotate(-10deg); 169 214 } 215 216 .application-filters { 217 gap: 15px; 218 border-radius: 8px; 219 background-color: white; 220 } 221 222 /* Default Span Styles */ 223 .application-filters span { 224 padding: 8px 12px; 225 border-radius: 5px; 226 color: rgba(1,38,90,0.9); 227 cursor: pointer; 228 display: inline-flex; 229 align-items: center; 230 } 231 232 /* Icon Styles */ 233 .application-filters span i { 234 margin-right: 5px; 235 } 236 237 /* Hover Effect */ 238 .application-filters span:hover { 239 background-color: rgba(1,38,90,0.9); 240 color: white; 241 } 242 243 .application-filters span:hover i { 244 color: inherit; /* Makes the icon inherit the color from the parent span */ 245 } 246 247 /* Selected State */ 248 .application-filters span.selected { 249 background-color: rgba(1,38,90,0.9); 250 color: white; 251 } 252 253 .application-filters span.selected i { 254 color: inherit; /* Ensures icon color matches the selected state */ 255 } -
jobvista-frontend/src/views/applications/ApplicationsByJobAd.js
r0f0add0 r4d97b63 25 25 const [jobAdTitle, setJobAdTitle] = useState(""); 26 26 27 const [changedApplications, setChangedApplications] = useState({}); 28 27 29 useEffect(() => { 28 if(!dispatched && (applicationsByJobAdState.length === 0 || applicationsByJobAdState.length === 1)) {30 if(!dispatched) { 29 31 dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => { 30 32 if (success && reponse.data.length > 0) { … … 60 62 }, [dispatched]) 61 63 64 62 65 const fetchProfilePic = (jobSeekerId) => { 63 66 dispatch(JobSeekerActions.downloadProfilePic(jobSeekerId, (success, response) => { … … 67 70 })) 68 71 } 69 70 72 71 73 const options = [ … … 76 78 ]; 77 79 80 const [selectedFilter, setSelectedFilter] = useState('All'); 81 82 const filters = [ 83 { value: 'ALL', label: 'All', icon: 'fa-folder-open' }, 84 { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' }, 85 { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' }, 86 { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' }, 87 { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' }, 88 ]; 89 90 const filterApplicationsByJobAdvertisement = (filter) => { 91 dispatch(ApplicationActions.filterApplicationsByJobAdId(advertisement_id, filter, (success, response) => { 92 if(success) { 93 //notify 94 } 95 })) 96 } 97 78 98 let handleDefaultStatus = (status) => { 79 99 return options.find(option => option.value === status); 80 100 } 81 101 82 let handleChangeStatus = (selectedOption, id) => { 83 dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => { 102 let handleStatusChange = (selectedOption, id) => { 103 104 const currentApplication = applicationsByJobAd.find(app => app.id === id); 105 106 setChangedApplications(prevState => ({ 107 ...prevState, 108 [id]: { 109 ...prevState[id], 110 response: (selectedOption.value === "ACCEPTED" || selectedOption.value === "DENIED") ? (prevState[id]?.response || currentApplication.response || "") : "", 111 status: selectedOption.value 112 } 113 })) 114 115 setApplicationsByJobAd(prevState => ( 116 prevState.map(application => 117 application.id === id ? {...application, status: selectedOption.value} : application) 118 )) 119 120 const responseTextarea = document.getElementById(`response-${id}`); 121 if (responseTextarea) { 122 responseTextarea.value = ""; 123 } 124 125 /* dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => { 84 126 if(success) { 85 // console.log("Status updated.")86 127 notifyAppStatusUpdate() 87 128 } 88 })) 89 } 129 }))*/ 130 } 131 132 let handleResponseChange = (e, id) => { 133 134 const currentApplication = applicationsByJobAd.find(app => app.id === id); 135 136 setChangedApplications(prevState => ({ 137 ...prevState, 138 [id]: { 139 ...prevState[id], 140 response: e.target.value, 141 status: prevState[id]?.status || currentApplication.status, 142 } 143 } 144 )) 145 } 146 147 const handleSaveChanges = () => { 148 console.log(changedApplications) 149 const changes = Object.entries(changedApplications).map( 150 ([applicationId, change]) => ({ 151 id: applicationId, 152 status: change.status, 153 response: change.response, 154 }) 155 ); 156 console.log(changes) 157 158 if(changes.length === 0) { 159 return; 160 } 161 162 dispatch(ApplicationActions.updateApplications(changes, (success, response) => { 163 if(success) { 164 setChangedApplications({}); 165 notifyAppStatusUpdate() 166 //notify change success 167 } 168 })) 169 170 171 } 172 173 const isChangedApplication = (id) => { 174 return changedApplications && Object.keys(changedApplications).includes(id.toString()); 175 }; 90 176 91 177 … … 98 184 </div> 99 185 186 <div className="row"> 187 <div className="col-md-6 application-filters-wrap"> 188 <div className="application-filters d-inline-flex flex-row justify-content-start"> 189 { 190 filters.map(filter => ( 191 <span 192 key={filter.label} 193 className={selectedFilter === filter.label ? "selected" : ""} 194 onClick={() => { 195 setSelectedFilter(filter.label) 196 filterApplicationsByJobAdvertisement(filter.value) 197 setChangedApplications({}); 198 }} 199 ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span> 200 )) 201 } 202 </div> 203 204 </div> 205 <div className="col-md-6 d-inline-flex flex-row justify-content-end"> 206 <button onClick={handleSaveChanges} 207 className={`blue-submit-button ${Object.keys(changedApplications).length === 0 ? 'disabled' : ''}`} 208 disabled={Object.keys(changedApplications).length === 0} 209 >Submit Changes</button> 210 </div> 211 </div> 212 213 214 100 215 {applicationsByJobAd && applicationsByJobAd.map((application, index) => ( 101 <div className="application-card"> 102 <div className="app-job-seeker-pic"> 103 <img 104 // loading gif 105 src={profilePicsState[application.jobSeekerId]} 106 alt="" 107 width={75} height={75} 108 /> 109 </div> 110 <div className="app-info"> 111 <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 112 day: 'numeric', 113 month: 'long', 114 year: 'numeric' 115 })}</b></span> 116 <div className="contact-info"> 117 <div className="contact-item"> 118 <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span> 119 </div> <div className="contact-item"> 120 <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span> 121 </div> <div className="contact-item"> 122 <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span> 216 <div 217 key={application.id} 218 className={`application-card-wrapper ${(application.status !== "PROPOSED" ) ? 'expanded' : ''}`} 219 > 220 221 <div className={`application-card ${changedApplications[application.id] ? 'changed' : ''}`}> 222 <div className="app-job-seeker-pic"> 223 <img 224 src={profilePicsState[application.jobSeekerId]} 225 alt="" 226 width={75} height={75} 227 /> 228 </div> 229 <div className="app-info"> 230 <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 231 day: 'numeric', 232 month: 'long', 233 year: 'numeric' 234 })}</b></span> 235 <div className="contact-info"> 236 <div className="contact-item"> 237 <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span> 238 </div> 239 <div className="contact-item"> 240 <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span> 241 </div> 242 <div className="contact-item"> 243 <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span> 244 </div> 245 </div> 246 </div> 247 248 <div className="app-status"> 249 <ApplicationDetailsModal application={application} /> 250 <div className="select"> 251 <Select options={options} onChange={(selectedOption) => handleStatusChange(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)} /> 123 252 </div> 124 253 </div> 125 254 </div> 126 255 127 <div className="app-status"> 128 <ApplicationDetailsModal application={application}/> 129 <div className="select"> 130 <Select options={options} onChange={(selectedOption) => handleChangeStatus(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)}/> 131 </div> 132 256 <div className="expand-section"> 257 <textarea 258 id={`response-${application.id}`} 259 placeholder={application.status === "UNDER_REVIEW" ? "Request additional documents..." :"Write your response..."} 260 defaultValue={application.response} 261 onChange={(e) => handleResponseChange(e, application.id)} 262 /> 133 263 </div> 134 264 </div> 135 ))} 265 ))} 266 136 267 137 268 </div>) -
jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js
r0f0add0 r4d97b63 22 22 23 23 24 25 24 useEffect(() => { 26 if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) {25 if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) { 27 26 dispatch(ApplicationActions.fetchApplicationsByJobSeeker(auth.id, (success, response) => { 28 if (success && response.data.length > 0) {27 if (success && response.data.length > 0) { 29 28 setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn")); 30 29 } … … 41 40 useEffect(() => { 42 41 43 if (dispatched && !logoDispatched) {42 if (dispatched && !logoDispatched) { 44 43 applicationsByJobSeeker.forEach(jobAd => { 45 if (jobAd.recruiterId && !logos[jobAd.recruiterId]) {44 if (jobAd.recruiterId && !logos[jobAd.recruiterId]) { 46 45 fetchLogo(jobAd.recruiterId); 47 46 } … … 49 48 setLogoDispatched(true) 50 49 console.log("Fetch all logos GET") 51 } else if (logoDispatched) {50 } else if (logoDispatched) { 52 51 setLogos(logosState) 53 52 console.log("Fetch all logos STATE") … … 58 57 59 58 60 61 59 const fetchLogo = (recruiterId) => { 62 60 dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => { 63 if (success) {61 if (success) { 64 62 setLogos(prevLogos => ({...prevLogos, [recruiterId]: response})) 65 63 } … … 67 65 }; 68 66 69 const options = [ 70 {value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i className="fa-solid fa-paper-plane"></i> Proposed</span>}, 71 {value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i className="fa-solid fa-file-pen"></i> Under Review</span>}, 72 {value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i className="fa-solid fa-user-check"></i> Accepted</span>}, 73 {value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i className="fa-solid fa-user-slash"></i> Denied</span>} 67 const options = [{ 68 value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i 69 className="fa-solid fa-paper-plane"></i> Proposed</span> 70 }, { 71 value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i 72 className="fa-solid fa-file-pen"></i> Under Review</span> 73 }, { 74 value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i 75 className="fa-solid fa-user-check"></i> Accepted</span> 76 }, { 77 value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i 78 className="fa-solid fa-user-slash"></i> Denied</span> 79 }]; 80 81 const [selectedFilter, setSelectedFilter] = useState('All'); 82 83 const filters = [ 84 { value: 'ALL', label: 'All', icon: 'fa-folder-open' }, 85 { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' }, 86 { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' }, 87 { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' }, 88 { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' }, 74 89 ]; 90 91 const filterApplicationsByJobSeeker= (filter) => { 92 dispatch(ApplicationActions.filterApplicationsByJobSeeker(auth.id, filter, (success, response) => { 93 if(success) { 94 //notify 95 } 96 })) 97 } 75 98 76 99 let handleDefaultValue = (status) => { … … 79 102 80 103 81 82 return ( 83 <div className="custom-container"> 104 return (<div className="custom-container"> 84 105 85 106 <div className="application-title"> 86 107 <h3>Application history</h3> 87 108 </div> 109 110 <div className="application-filters d-inline-flex flex-row justify-content-start"> 111 { 112 filters.map(filter => ( 113 <span 114 key={filter.label} 115 className={selectedFilter === filter.label ? "selected" : ""} 116 onClick={() => { 117 setSelectedFilter(filter.label) 118 filterApplicationsByJobSeeker(filter.value) 119 }} 120 ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span> 121 )) 122 } 123 </div> 124 88 125 {applicationsByJobSeeker && applicationsByJobSeeker.map((application, index) => ( 89 <div key={index} className="application-card"> 90 <div className="app-company-logo"> 91 <img 92 // loading gif 93 src={logosState[application.recruiterId]} 94 alt="" 95 width={75} height={75} 96 /> 97 </div> 126 <div className="application-card-wrapper"> 127 <div key={index} className="application-card"> 128 <div className="app-company-logo"> 129 <img 130 // loading gif 131 src={logosState[application.recruiterId]} 132 alt="" 133 width={75} height={75} 134 /> 135 </div> 98 136 99 <div className="app-info"> 100 <Link to={`/job-advertisements/${application.jobAdId}`} className="jobAd-title">{application.jobAdTitle}</Link> 101 {/*<h5 className="jobAd-title"></h5>*/} 102 <div className="contact-info"> 103 <div className="contact-item"> 104 <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span> 137 <div className="app-info"> 138 <Link to={`/job-advertisements/${application.jobAdId}`} 139 className="jobAd-title">{application.jobAdTitle}</Link> 140 {/*<h5 className="jobAd-title"></h5>*/} 141 <div className="contact-info"> 142 <div className="contact-item"> 143 <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span> 144 </div> 145 <div className="contact-item"> 146 <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span> 147 </div> 148 <div className="contact-item"> 149 <i className="fa-solid fa-phone"></i> 150 <span>{application.recruiterPhoneNumber}</span> 151 </div> 152 <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 153 day: 'numeric', month: 'long', year: 'numeric' 154 })}</b></span> 105 155 </div> 106 <div className="contact-item"> 107 <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span> 108 </div> 109 <div className="contact-item"> 110 <i className="fa-solid fa-phone"></i> <span>{application.recruiterPhoneNumber}</span> 111 </div> 112 <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 113 day: 'numeric', 114 month: 'long', 115 year: 'numeric' 116 })}</b></span> 156 </div> 157 158 <div className="app-status"> 159 <ApplicationDetailsModal application={application}/> 160 <> {handleDefaultValue(application.status).label}</> 161 {/*<div className="select">*/} 162 {/* <Select isDisabled={true} options={options} />*/} 163 {/*</div>*/} 164 117 165 </div> 118 166 </div> 167 {application.response && 168 <div className="response-message"> 169 {application.response} 170 </div> 171 } 119 172 120 <div className="app-status"> 121 <ApplicationDetailsModal application={application}/> 122 <> {handleDefaultValue(application.status).label}</> 123 {/*<div className="select">*/} 124 {/* <Select isDisabled={true} options={options} />*/} 125 {/*</div>*/} 173 </div> 126 174 127 </div>128 </div>129 175 ))} 130 176 131 </div> 132 ) 177 </div>) 133 178 } -
jobvista-frontend/src/views/applications/ApplyToJobAdModal.js
r0f0add0 r4d97b63 105 105 onChange={(e) => setResumeFile(e.target.files[0])} required type="file" 106 106 id="fileUpload" accept=".pdf"/> 107 108 107 <br/> 109 108 <label className="label">Message to the recruiter</label> -
jobvista-frontend/src/views/auth/SignInForm.js
r0f0add0 r4d97b63 1 import {Button, TextField} from "@mui/material";2 1 import {Link} from "react-router-dom"; 3 2 import "./auth.css" … … 9 8 import {AuthActions} from "../../redux/actions/authActions"; 10 9 import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils"; 10 11 import {GoogleOAuthProvider, GoogleLogin} from "@react-oauth/google"; 11 12 12 13 export const SignInForm = () => { … … 40 41 } 41 42 43 const handleGoogleSuccess = (response) => { 44 const tokenId = response.credential; 45 46 dispatch(AuthActions.signInGoogle(tokenId, (success, error) => { 47 if (success) { 48 console.log("User signed in successfully"); 49 if(success) { 50 navigate("/") 51 } 52 } else { 53 console.error("Google sign-in failed", error); 54 } 55 })); 56 }; 57 58 const handleGoogleFailure = (error) => { 59 console.error(error); 60 }; 61 42 62 return ( 43 63 44 <div className=" d-flex align-items-center">64 <div className=""> 45 65 <div className="container"> 46 <div className="row ">47 <div className="col-md-8 mx-autoform-container">66 <div className="row d-flex flex-column justify-content-center align-items-center"> 67 <div className="col-md-8 form-container"> 48 68 <h3 className="login-heading mb-4">Sign in</h3> 49 69 <form onSubmit={handleSubmit(signIn)}> … … 83 103 84 104 <div className="row"> 105 <GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}> 106 <GoogleLogin 107 onSuccess={handleGoogleSuccess} 108 onError={handleGoogleFailure} 109 type={"standard"} 110 text={"signin_with"} 111 locale={"en"} 112 redirectUri="http://localhost:3000/login/oauth2/code/google" 113 /> 114 </GoogleOAuthProvider> 115 </div> 116 <br/> 117 </div> 118 119 <div className="col-md-8 mt-5 form-container"> 120 <div> 121 <h5 className="mb-3">Don't have an account?</h5> 122 </div> 123 124 <div className="row"> 85 125 <div className="col-md-6"> 86 126 <Link to="/signup/recruiter" className="btn auth-secondary-btn text-uppercase fw-bold mb-2 w-100">SIGN UP AS RECRUITER</Link> … … 91 131 </div> 92 132 </div> 133 93 134 </div> 94 135 </div> -
jobvista-frontend/src/views/auth/auth.css
r0f0add0 r4d97b63 4 4 } 5 5 6 .form-container {7 margin-bottom: 80px;8 }9 6 10 7 .auth-primary-btn{ … … 27 24 color: white; 28 25 } 26 27 iframe{ 28 margin: auto !important; 29 } -
jobvista-frontend/src/views/dashboard/Dashboard.js
r0f0add0 r4d97b63 141 141 </div> 142 142 <div className="card-body"> 143 <img 143 <img className="card-company-logo" 144 144 // loading gif 145 145 src={logos[jobAd.recruiterId]} -
jobvista-frontend/src/views/job_advertisements/JobAdDetails.js
r0f0add0 r4d97b63 85 85 <> 86 86 <img 87 className="card-company-logo" 87 88 // loading gif 88 89 src={logosState[jobAd.recruiterId]} -
jobvista-frontend/src/views/shared_css/Modal.css
r0f0add0 r4d97b63 65 65 .react-responsive-modal-modal .modal-content form .label { 66 66 display: block; 67 margin-bottom: 10px; 68 font-weight: 500; 67 69 } 68 70 -
jobvista-frontend/src/views/static/Header.js
r0f0add0 r4d97b63 117 117 {user.role === Roles.RECRUITER && <img src={logoState[auth.id]} /> } 118 118 {user.role === Roles.ADMIN && <img src="/images/admin.jpg"/> } 119 {/*<img src="https://lh3.googleusercontent.com/a/ACg8ocJOmmRzyRWcuhJj_sCzIoxMeP1M1DOgQ1UeYsFoeJuFB4XgOAnS=s96-c"/>*/} 119 120 </div> 120 121 <div className="auth-box">
Note:
See TracChangeset
for help on using the changeset viewer.