Ignore:
Timestamp:
08/30/24 15:44:27 (4 weeks ago)
Author:
223021 <daniel.ilievski.2@…>
Branches:
main
Parents:
0f0add0
Message:

Implemented Google login, additional file uploads, response messages and email notifications

Location:
jobvista-frontend/src
Files:
17 edited

Legend:

Unmodified
Added
Removed
  • jobvista-frontend/src/App.css

    r0f0add0 r4d97b63  
    3838
    3939.form-container {
     40  font-family: "Segoe UI";
    4041  background-color: white;
    4142  border-radius: 10px;
    4243  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
    4451}
    4552
     
    217224}
    218225
    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  
    1313export const FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER = "FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER"
    1414export const SUBMIT_APPLICATION = "SUBMIT_APPLICATION"
     15export const UPDATE_APPLICATION = "UPDATE_APPLICATION"
    1516export const UPDATE_APPLICATION_STATUS = "UPDATE_APPLICATION_STATUS"
     17export const UPDATE_APPLICATIONS = "UPDATE_APPLICATIONS"
    1618export const FETCH_APPLICATIONS_BY_JOB_ID = "FETCH_APPLICATIONS_BY_JOB_ID"
     19export const FILTER_APPLICATIONS_BY_JOB_ID = "FILTER_APPLICATIONS_BY_JOB_ID"
    1720export const FETCH_APPLICATIONS_BY_JOB_SEEKER_ID = "FETCH_APPLICATIONS_BY_JOB_SEEKER_ID"
     21export const FILTER_APPLICATIONS_BY_JOB_SEEKER_ID = "FILTER_APPLICATIONS_BY_JOB_SEEKER_ID"
    1822export const DOWNLOAD_RESUME = "DOWNLOAD_RESUME"
    1923
  • jobvista-frontend/src/redux/actions/applicationActions.js

    r0f0add0 r4d97b63  
    33    CURRENT_USER,
    44    FETCH_APPLICATIONS_BY_JOB_ID,
    5     FETCH_APPLICATIONS_BY_JOB_SEEKER_ID,
    6     SUBMIT_APPLICATION, UPDATE_APPLICATION_STATUS
     5    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
    77} from "../actionTypes";
    88
     
    1818                    dispatch({
    1919                        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,
    2041                        application: response.data
    2142                    })
     
    4566        }
    4667    },
     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
    4784    fetchApplicationsByJobSeeker: (jobSeekerId, callback) => {
    4885        return dispatch => {
     
    5188                    dispatch({
    5289                        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,
    53105                        applicationsByJobSeeker: response.data
    54106                    })
     
    75127        }
    76128    },
     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    },
    77145    downloadResume: (id, callback) => {
    78146        return axios.get("/applications/" + id + "/download-resume", {responseType: "blob"})
     
    86154            })
    87155
     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
    88167    }
    89168}
  • jobvista-frontend/src/redux/actions/authActions.js

    r0f0add0 r4d97b63  
    3232            }).catch((error) => {
    3333                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);
    3460            });
    3561        };
  • jobvista-frontend/src/redux/reducers/applicationReducer.js

    r0f0add0 r4d97b63  
    22    CURRENT_USER,
    33    FETCH_APPLICATIONS_BY_JOB_ID,
    4     FETCH_APPLICATIONS_BY_JOB_SEEKER_ID,
    5     SUBMIT_APPLICATION, UPDATE_APPLICATION_STATUS
     4    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
    66} from "../actionTypes";
    77
     
    2222                applicationsByJobSeeker: [...state.applicationsByJobSeeker, action.application]
    2323            }
     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            }
    2433        case UPDATE_APPLICATION_STATUS:
    2534            return {
     
    3645                applicationsByJobAdId: action.applicationsByJobAdId
    3746            }
     47        case FILTER_APPLICATIONS_BY_JOB_ID:
     48            return {
     49                ...state,
     50                applicationsByJobAdId: action.applicationsByJobAdId
     51            }
     52
    3853        case FETCH_APPLICATIONS_BY_JOB_SEEKER_ID:
     54            return {
     55                ...state,
     56                applicationsByJobSeeker: action.applicationsByJobSeeker
     57            }
     58        case FILTER_APPLICATIONS_BY_JOB_SEEKER_ID:
    3959            return {
    4060                ...state,
  • jobvista-frontend/src/utils/toastUtils.js

    r0f0add0 r4d97b63  
    108108    toast.success(
    109109        <span>
    110             Status updated successfully!
     110            Application/s updated successfully!
    111111        </span>, {
    112112            position: "bottom-right",
     
    126126        <span>
    127127            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
     141export const notifyJobAdUpdate= () => {
     142    toast.success(
     143        <span>
     144            Your application was successfully updated!
    128145        </span>, {
    129146            position: "bottom-right",
  • jobvista-frontend/src/views/applications/ApplicationDetailsModal.js

    r0f0add0 r4d97b63  
    1717import Roles from "../../enumerations/Roles";
    1818import {ApplicationActions} from "../../redux/actions/applicationActions";
     19import {notifyJobAdApply, notifyJobAdUpdate} from "../../utils/toastUtils";
    1920
    2021
     
    2526    const auth = useSelector(state => state.auth.currentUser)
    2627    const [resumeUrl, setResumeUrl] = useState("");
     28    const [additionalFileUrls, setAdditionalFileUrls] = useState([]);
    2729
    28     //const [resumeFile, setResumeFile] = useState(null);
     30    const [additionalFiles, setAdditionalFiles] = useState(null);
    2931    const toggleModal = () => {
    3032        setModal(!modal);
    3133    };
     34
     35    const {register, handleSubmit, control, formState: {errors}} = useForm();
    3236
    3337    useEffect(() => {
     
    3640                if (success) {
    3741                    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                    }
    3850                }
    3951            })
    4052        }
    4153    }, [])
     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    }
    4274
    4375    function getFileName(path) {
     
    5082
    5183    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
    5389        <Modal open={modal} onClose={toggleModal} center>
    5490            <div className="head-modal">
     
    5894
    5995            <div className="modal-content">
    60                 <form>
     96                <form onSubmit={handleSubmit(updateApplication)}>
    6197                    <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
    75118
    76119                        </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>
    83123
    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
    88162
    89163                        </div>
    90164                    </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>}
    91170
    92171                </form>
     172
     173
    93174            </div>
    94175        </Modal>
  • jobvista-frontend/src/views/applications/Applications.css

    r0f0add0 r4d97b63  
    1717}
    1818
    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
    2366
    2467.application-card {
     
    2972    transition: all 0.3s ease;
    3073    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    31     height: auto;
     74    /*height: auto;*/
    3275    padding: 20px 20px;
    3376    display: flex;
    34     /*justify-content: center;*/
    35     /*gap: 20px;*/
    36     margin: 15px 0;
    37     /*z-index: -1000;*/
    3877}
    3978.application-card .app-job-seeker-pic {
     
    5190
    5291.application-card .app-info {
    53     width: 65%;
     92    width: 60%;
    5493    display: inline-flex;
    5594    flex-direction: column;
     
    86125
    87126.application-card .app-status {
    88     width: 28%;
     127    width: 38%;
    89128    display: inline-flex;
    90129    justify-content: end;
     
    98137}
    99138
    100 .status {
     139.app-status .status {
    101140    color: white;
    102141    padding: 5px 10px;
     
    105144    text-align: center;
    106145}
     146
     147
     148
     149
     150
     151
    107152
    108153
     
    168213    transform: rotate(-10deg);
    169214}
     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  
    2525    const [jobAdTitle, setJobAdTitle] = useState("");
    2626
     27    const [changedApplications, setChangedApplications] = useState({});
     28
    2729    useEffect(() => {
    28         if(!dispatched && (applicationsByJobAdState.length === 0 || applicationsByJobAdState.length === 1)) {
     30        if(!dispatched) {
    2931            dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => {
    3032                if (success && reponse.data.length > 0) {
     
    6062    }, [dispatched])
    6163
     64
    6265    const fetchProfilePic = (jobSeekerId) => {
    6366        dispatch(JobSeekerActions.downloadProfilePic(jobSeekerId, (success, response) => {
     
    6770        }))
    6871    }
    69 
    7072
    7173    const options = [
     
    7678    ];
    7779
     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
    7898    let handleDefaultStatus = (status) => {
    7999        return options.find(option => option.value === status);
    80100    }
    81101
    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) => {
    84126            if(success) {
    85                 // console.log("Status updated.")
    86127                notifyAppStatusUpdate()
    87128            }
    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    };
    90176
    91177
     
    98184        </div>
    99185
     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
    100215        {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)} />
    123252                        </div>
    124253                    </div>
    125254                </div>
    126255
    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                />
    133263                </div>
    134264            </div>
    135        ))}
     265        ))}
     266
    136267
    137268    </div>)
  • jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js

    r0f0add0 r4d97b63  
    2222
    2323
    24 
    2524    useEffect(() => {
    26         if(!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1) ) {
     25        if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) {
    2726            dispatch(ApplicationActions.fetchApplicationsByJobSeeker(auth.id, (success, response) => {
    28                 if(success && response.data.length > 0) {
     27                if (success && response.data.length > 0) {
    2928                    setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn"));
    3029                }
     
    4140    useEffect(() => {
    4241
    43         if(dispatched && !logoDispatched) {
     42        if (dispatched && !logoDispatched) {
    4443            applicationsByJobSeeker.forEach(jobAd => {
    45                 if(jobAd.recruiterId && !logos[jobAd.recruiterId]) {
     44                if (jobAd.recruiterId && !logos[jobAd.recruiterId]) {
    4645                    fetchLogo(jobAd.recruiterId);
    4746                }
     
    4948            setLogoDispatched(true)
    5049            console.log("Fetch all logos GET")
    51         } else if (logoDispatched){
     50        } else if (logoDispatched) {
    5251            setLogos(logosState)
    5352            console.log("Fetch all logos STATE")
     
    5857
    5958
    60 
    6159    const fetchLogo = (recruiterId) => {
    6260        dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => {
    63             if(success) {
     61            if (success) {
    6462                setLogos(prevLogos => ({...prevLogos, [recruiterId]: response}))
    6563            }
     
    6765    };
    6866
    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' },
    7489    ];
     90
     91    const filterApplicationsByJobSeeker= (filter) => {
     92        dispatch(ApplicationActions.filterApplicationsByJobSeeker(auth.id, filter, (success, response) => {
     93            if(success) {
     94                //notify
     95            }
     96        }))
     97    }
    7598
    7699    let handleDefaultValue = (status) => {
     
    79102
    80103
    81 
    82     return (
    83         <div className="custom-container">
     104    return (<div className="custom-container">
    84105
    85106            <div className="application-title">
    86107                <h3>Application history</h3>
    87108            </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
    88125            {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>
    98136
    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>
    105155                            </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
    117165                        </div>
    118166                    </div>
     167                    {application.response &&
     168                        <div className="response-message">
     169                            {application.response}
     170                        </div>
     171                    }
    119172
    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>
    126174
    127                     </div>
    128                 </div>
    129175            ))}
    130176
    131         </div>
    132     )
     177        </div>)
    133178}
  • jobvista-frontend/src/views/applications/ApplyToJobAdModal.js

    r0f0add0 r4d97b63  
    105105                                   onChange={(e) => setResumeFile(e.target.files[0])} required type="file"
    106106                                   id="fileUpload" accept=".pdf"/>
    107 
    108107                            <br/>
    109108                            <label className="label">Message to the recruiter</label>
  • jobvista-frontend/src/views/auth/SignInForm.js

    r0f0add0 r4d97b63  
    1 import {Button, TextField} from "@mui/material";
    21import {Link} from "react-router-dom";
    32import "./auth.css"
     
    98import {AuthActions} from "../../redux/actions/authActions";
    109import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils";
     10
     11import {GoogleOAuthProvider, GoogleLogin} from "@react-oauth/google";
    1112
    1213export const SignInForm = () => {
     
    4041    }
    4142
     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
    4262    return (
    4363
    44         <div className="d-flex align-items-center">
     64        <div className="">
    4565            <div className="container">
    46                 <div className="row">
    47                     <div className="col-md-8 mx-auto form-container">
     66                <div className="row d-flex flex-column justify-content-center align-items-center">
     67                    <div className="col-md-8 form-container">
    4868                        <h3 className="login-heading mb-4">Sign in</h3>
    4969                        <form onSubmit={handleSubmit(signIn)}>
     
    83103
    84104                        <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">
    85125                            <div className="col-md-6">
    86126                                <Link to="/signup/recruiter" className="btn auth-secondary-btn text-uppercase fw-bold mb-2 w-100">SIGN UP AS RECRUITER</Link>
     
    91131                        </div>
    92132                    </div>
     133
    93134                </div>
    94135            </div>
  • jobvista-frontend/src/views/auth/auth.css

    r0f0add0 r4d97b63  
    44}
    55
    6 .form-container {
    7     margin-bottom: 80px;
    8 }
    96
    107.auth-primary-btn{
     
    2724    color: white;
    2825}
     26
     27iframe{
     28    margin: auto !important;
     29}
  • jobvista-frontend/src/views/dashboard/Dashboard.js

    r0f0add0 r4d97b63  
    141141                                </div>
    142142                                <div className="card-body">
    143                                     <img
     143                                    <img className="card-company-logo"
    144144                                        // loading gif
    145145                                        src={logos[jobAd.recruiterId]}
  • jobvista-frontend/src/views/job_advertisements/JobAdDetails.js

    r0f0add0 r4d97b63  
    8585                                <>
    8686                                    <img
     87                                        className="card-company-logo"
    8788                                        // loading gif
    8889                                        src={logosState[jobAd.recruiterId]}
  • jobvista-frontend/src/views/shared_css/Modal.css

    r0f0add0 r4d97b63  
    6565.react-responsive-modal-modal .modal-content form .label {
    6666    display: block;
     67    margin-bottom: 10px;
     68    font-weight: 500;
    6769}
    6870
  • jobvista-frontend/src/views/static/Header.js

    r0f0add0 r4d97b63  
    117117                                    {user.role === Roles.RECRUITER && <img src={logoState[auth.id]} /> }
    118118                                    {user.role === Roles.ADMIN && <img src="/images/admin.jpg"/> }
     119                                    {/*<img src="https://lh3.googleusercontent.com/a/ACg8ocJOmmRzyRWcuhJj_sCzIoxMeP1M1DOgQ1UeYsFoeJuFB4XgOAnS=s96-c"/>*/}
    119120                                </div>
    120121                                <div className="auth-box">
Note: See TracChangeset for help on using the changeset viewer.