Ignore:
Timestamp:
06/17/24 21:59:14 (2 weeks ago)
Author:
223021 <daniel.ilievski.2@…>
Branches:
main
Children:
08f82ec
Parents:
b248810
Message:

Added an edit profile page for both job seekers and recruiters, where they can upload profile pictures/company logos and edit their profile data. Added profile page specifically for recruiters. Refactored existing code.

Location:
jobvista-frontend/src/views/applications
Files:
4 edited
1 moved

Legend:

Unmodified
Added
Removed
  • jobvista-frontend/src/views/applications/ApplicationDetailsModal.js

    rb248810 rbefb988  
    11import React, {useEffect, useState} from "react";
    2 import "../job_advertisements/Form.css";
     2import "../shared_css/Modal.css";
    33
    44import 'react-responsive-modal/styles.css';
     
    1919
    2020
    21 export const ViewApplicationDetails = (props) => {
     21export const ApplicationDetailsModal = (props) => {
    2222    const {application} = props
    2323    const [modal, setModal] = useState(false);
     
    5454                        <div className="col-md-6">
    5555                            <label className="label">Why are you interested in joining our company?</label>
    56                             <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled placeholder="Write your answer here..." className="applictaion-textarea"/>
     56                            <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled placeholder="Write your answer here..." className="application-textarea"/>
    5757
    5858
    5959                            <label className="label">What makes you a good fit for this position?</label>
    60                             <textarea disabled type="text" defaultValue={application.questionAnswers[1]}  placeholder="Write your answer here..." className="applictaion-textarea"/>
     60                            <textarea disabled type="text" defaultValue={application.questionAnswers[1]}  placeholder="Write your answer here..." className="application-textarea"/>
    6161
    6262
    6363                            <label className="label">What do you hope to achieve in your first 6 months in this role?</label>
    64                             <textarea disabled type="text" defaultValue={application.questionAnswers[2]}  placeholder="Write your answer here..." className="applictaion-textarea"/>
     64                            <textarea disabled type="text" defaultValue={application.questionAnswers[2]}  placeholder="Write your answer here..." className="application-textarea"/>
    6565
    6666                        </div>
     
    7373                            <br/>
    7474                            <label className="label">Message to the recruiter</label>
    75                             <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." className="applictaion-textarea"/>
     75                            <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." className="application-textarea"/>
    7676
    7777                        </div>
  • jobvista-frontend/src/views/applications/Applications.css

    rb248810 rbefb988  
    1 .applications-container {
     1.custom-container {
    22    width: 65% !important;
    33    max-width: 1500px !important;
     
    66
    77.application-title {
    8     font-family: Ubuntu;
    9     text-transform: uppercase;
    10     margin: 25px auto;
     8    font-family: "Segoe UI";
     9    margin-top: 40px;
     10    margin-bottom: 20px;
    1111    display: flex;
    12     justify-content: center;
     12    justify-content: start;
     13    border-bottom: 1px solid #CFCFCF;
    1314}
    14 .application-title h1 {
    15     font-weight: bold;
     15.application-title h3 {
     16    font-weight: 500;
    1617}
    1718
     
    2829    transition: all 0.3s ease;
    2930    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    30     transform: translate(0, 0);
    3131    height: auto;
    3232    padding: 20px 20px;
    3333    display: flex;
    3434    justify-content: center;
     35    gap: 20px;
    3536    margin: 15px 0;
     37    /*z-index: -1000;*/
     38}
     39.application-card .app-job-seeker-pic {
     40    border: 1px solid gray;
     41    border-radius: 50%
     42}
     43.application-card .app-job-seeker-pic img {
     44    border-radius: 50%;
    3645}
    3746
    38 .application-card .left-box {
    39     width: 55%;
    40     display: inline-block;
     47.application-card .app-info {
     48    width: 60%;
     49    display: inline-flex;
     50    flex-direction: column;
     51    align-items: flex-start;
     52    justify-content: center;
     53    gap: 10px;
    4154}
    4255
    43 .application-card .left-box .jobAd-title {
     56.application-card .app-info .jobAd-title {
    4457    font-weight: 600;
    4558    /*text-transform: uppercase;*/
    4659    font-family: 'Segoe UI', sans-serif;
    47     font-size: 22px;
     60    /*font-size: 22px;*/
    4861}
     62.application-card .app-info .jobAd-title
    4963
    50 .application-card .left-box .contact-info {
     64.application-card .app-info .contact-info {
    5165    display: inline-flex;
    52     gap: 10px;
     66    gap: 30px;
    5367}
    54 .application-card .left-box .contact-info .contact-item {
     68.application-card .app-info .contact-info .contact-item {
    5569    display: inline-flex;
    5670    align-items: center;
    5771    gap: 5px;
     72    margin-right: 10px
    5873}
    5974
    60 .application-card .right-box {
    61     width: 45%;
     75.application-card .app-info .contact-info .contact-item span {
     76   color: darkgray;
     77}
     78
     79.application-card .app-status {
     80    width: 40%;
    6281    display: inline-flex;
    6382    justify-content: end;
     
    6685}
    6786
    68 .application-card .right-box .select {
    69     width: 35%;
     87.application-card .app-status .select {
     88    width: 45%;
    7089    display: inline-block;
    7190}
     91
     92.status {
     93    color: white;
     94    padding: 5px 10px;
     95    border-radius: 5px;
     96    width: 150px;
     97    text-align: center;
     98}
     99
    72100
    73101.application-button {
     
    75103    border-radius: 8px;
    76104    width: auto;
    77     background-color: rgba(207, 235, 255, 1);
    78     color: black;
    79     font-weight: bold;
    80     padding: 5px 10px;
    81     transition: 0.2s;
     105    background-image: linear-gradient(to bottom, #dddddd, #f0f0f0);
     106    background-color: #dddddd;
     107    color: #333333;
     108
     109    font-weight: 700;
     110    padding: 8px 13px;
    82111}
     112
    83113.application-button:hover {
    84     background-color: rgb(187, 215, 235);
     114    background: linear-gradient(to bottom, #bbbbbb, #dddddd);
     115    color: #333333;
    85116}
    86117
  • jobvista-frontend/src/views/applications/ApplicationsByJobAd.js

    rb248810 rbefb988  
    1 import {useDispatch} from "react-redux";
     1import {useDispatch, useSelector} from "react-redux";
    22import {useEffect, useState} from "react";
    33import {useParams} from "react-router";
    44import {ApplicationActions} from "../../redux/actions/applicationActions";
    5 import {ViewApplicationDetails} from "./ViewApplicationDetails";
     5import {ApplicationDetailsModal} from "./ApplicationDetailsModal";
    66import "./Applications.css"
    77import Select from "react-select";
     8import {sortElementsBy} from "../../utils/utils";
     9import {JobSeekerActions} from "../../redux/actions/JobSeekerActions";
     10import {notifyAppStatusUpdate} from "../../utils/toastUtils";
    811
    912export const ApplicationsByJobAd = () => {
     
    1114    const dispatch = useDispatch();
    1215    let {advertisement_id} = useParams();
     16
    1317    const [applicationsByJobAd, setApplicationsByJobAd] = useState([]);
     18    let applicationsByJobAdState = useSelector(state => state.appl.applicationsByJobAdId)
     19    const [dispatched, setDispatched] = useState(false);
     20
     21    const [profilePics, setProfilePics] = useState({});
     22    let profilePicsState = useSelector(state => state.images.profilePictures)
     23    const [profilePicsDispatched, setProfilePicsDispatched] = useState(false);
     24
    1425    const [jobAdTitle, setJobAdTitle] = useState("");
    1526
    1627    useEffect(() => {
    17         dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => {
    18             if (success && reponse.data.length > 0) {
    19                 setApplicationsByJobAd(reponse.data)
    20                 setJobAdTitle(reponse.data[0].jobAdTitle)
     28        if(!dispatched && (applicationsByJobAdState.length === 0 || applicationsByJobAdState.length === 1)) {
     29            dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => {
     30                if (success && reponse.data.length > 0) {
     31                    setApplicationsByJobAd(sortElementsBy(reponse.data, "submittedOn"))
     32                    setJobAdTitle(reponse.data[0].jobAdTitle)
     33                }
     34                setDispatched(true)
     35                console.log("Fetch applications by job ad GET")
     36            }))
     37        } else {
     38            setApplicationsByJobAd(sortElementsBy(applicationsByJobAdState, "submittedOn"));
     39            if(applicationsByJobAdState.length > 0) {
     40                setJobAdTitle(applicationsByJobAdState[0].jobAdTitle)
     41            }
     42
     43        }
     44
     45    }, [applicationsByJobAdState])
     46
     47    useEffect(() => {
     48        if(dispatched && !profilePicsDispatched) {
     49            applicationsByJobAd.forEach(app => {
     50                if(app.jobSeekerId && !profilePics[app.jobSeekerId]) {
     51                    fetchProfilePic(app.jobSeekerId)
     52                }
     53            })
     54            setProfilePicsDispatched(true);
     55            console.log("Fetch all profile pics GET")
     56        } else if(profilePicsDispatched) {
     57            setProfilePics(profilePicsState)
     58            console.log("Fetch all profile pics STATE")
     59        }
     60    }, [dispatched])
     61
     62    const fetchProfilePic = (jobSeekerId) => {
     63        dispatch(JobSeekerActions.downloadProfilePic(jobSeekerId, (success, response) => {
     64            if(success) {
     65                setProfilePics(prevState => ({...prevState, [jobSeekerId]: response}))
    2166            }
    2267        }))
    23     }, [])
     68    }
     69
    2470
    2571    const options = [
     
    3783        dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => {
    3884            if(success) {
    39                 console.log("Status updated.")
     85                // console.log("Status updated.")
     86                notifyAppStatusUpdate()
    4087            }
    4188        }))
     
    4390
    4491
    45     return (<div className="applications-container">
     92    return (<div className="custom-container">
    4693        <div className="application-title">
    4794            {jobAdTitle ?
    48                 <h1>Applications for <span>{jobAdTitle}</span></h1> :
     95                <h3>Applications for <b>{jobAdTitle}</b></h3> :
    4996                <h1></h1>
    5097            }
    51 
    5298        </div>
    5399
    54100        {applicationsByJobAd && applicationsByJobAd.map((application, index) => (
    55101            <div className="application-card">
    56                 <div className="left-box">
     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">
    57111                    <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', {
    58112                        day: 'numeric',
     
    60114                        year: 'numeric'
    61115                    })}</b></span>
    62                     <br/><br/>
    63116                    <div className="contact-info">
    64117                        <div className="contact-item">
    65118                            <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span>
    66                         </div>
    67                         <div className="contact-item">
     119                        </div> <div className="contact-item">
    68120                            <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span>
    69                         </div>
    70                         <div className="contact-item">
     121                        </div> <div className="contact-item">
    71122                            <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span>
    72123                        </div>
     
    74125                </div>
    75126
    76                 <div className="right-box">
    77                     <ViewApplicationDetails application={application}/>
     127                <div className="app-status">
     128                    <ApplicationDetailsModal application={application}/>
    78129                    <div className="select">
    79130                        <Select options={options}  onChange={(selectedOption) => handleChangeStatus(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)}/>
  • jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js

    rb248810 rbefb988  
    22import {useEffect, useState} from "react";
    33import {ApplicationActions} from "../../redux/actions/applicationActions";
    4 import {ViewApplicationDetails} from "./ViewApplicationDetails";
     4import {ApplicationDetailsModal} from "./ApplicationDetailsModal";
    55import Select from "react-select";
     6
     7import {RecruiterActions} from "../../redux/actions/recruiterActions";
     8import {sortElementsBy} from "../../utils/utils";
    69
    710export const ApplicationsByJobSeeker = () => {
    811    const dispatch = useDispatch();
     12    const auth = useSelector(state => state.auth.currentUser);
     13
    914    const [applicationsByJobSeeker, setApplicationsByJobSeeker] = useState([]);
    1015    let applicationsByJobSeekerState = useSelector(state => state.appl.applicationsByJobSeeker);
    1116    const [dispatched, setDispatched] = useState(false);
    1217
     18    const [logos, setLogos] = useState({});
     19    let logosState = useSelector(state => state.images.logos)
     20    const [logoDispatched, setLogoDispatched] = useState(false);
     21
     22
     23
    1324    useEffect(() => {
    14         if(!dispatched && applicationsByJobSeekerState.length === 0) {
    15             dispatch(ApplicationActions.fetchApplicationsByJobSeeker((success, response) => {
     25        if(!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1) ) {
     26            dispatch(ApplicationActions.fetchApplicationsByJobSeeker(auth.id, (success, response) => {
    1627                if(success && response.data.length > 0) {
    17                     setApplicationsByJobSeeker(response.data);
     28                    setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn"));
    1829                }
     30                setDispatched(true)
    1931                console.log("Fetch applications by job seeker GET")
    2032            }))
    21             setDispatched(true)
     33
    2234        } else {
    23             setApplicationsByJobSeeker(applicationsByJobSeekerState);
     35            setApplicationsByJobSeeker(sortElementsBy(applicationsByJobSeekerState, "submittedOn"));
    2436            console.log("Fetch applications by job seeker STATE")
    2537        }
    2638    }, [applicationsByJobSeekerState])
    2739
     40    useEffect(() => {
     41
     42        if(dispatched && !logoDispatched) {
     43            applicationsByJobSeeker.forEach(jobAd => {
     44                if(jobAd.recruiterId && !logos[jobAd.recruiterId]) {
     45                    fetchLogo(jobAd.recruiterId);
     46                }
     47            })
     48            setLogoDispatched(true)
     49            console.log("Fetch all logos GET")
     50        } else if (logoDispatched){
     51            setLogos(logosState)
     52            console.log("Fetch all logos STATE")
     53
     54        }
     55
     56    }, [dispatched, logosState])
     57
     58
     59
     60    const fetchLogo = (recruiterId) => {
     61        dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => {
     62            if(success) {
     63                setLogos(prevLogos => ({...prevLogos, [recruiterId]: response}))
     64            }
     65        }));
     66    };
     67
    2868    const options = [
    29         {value: 'PROPOSED', label: <span><i className="fa-solid fa-paper-plane"></i> Proposed</span>},
    30         {value: 'UNDER_REVIEW', label: <span><i className="fa-solid fa-file-pen"></i> Under Review</span>},
    31         {value: 'ACCEPTED', label: <span><i className="fa-solid fa-user-check"></i> Accepted</span>},
    32         {value: 'DENIED', label: <span><i className="fa-solid fa-user-slash"></i> Denied</span>}
     69        {value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i className="fa-solid fa-paper-plane"></i> Proposed</span>},
     70        {value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i className="fa-solid fa-file-pen"></i> Under Review</span>},
     71        {value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i className="fa-solid fa-user-check"></i> Accepted</span>},
     72        {value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i className="fa-solid fa-user-slash"></i> Denied</span>}
    3373    ];
    3474
     
    4080
    4181    return (
    42         <div className="applications-container">
     82        <div className="custom-container">
     83
    4384            <div className="application-title">
    44 
     85                <h3>Application history</h3>
    4586            </div>
    4687            {applicationsByJobSeeker && applicationsByJobSeeker.map((application, index) => (
    4788                <div key={index} className="application-card">
    48                     <div className="left-box">
    49                         <span className="jobAd-title">{application.jobAdTitle}</span>
    50                         <br/>
    51                         <br/>
     89                    <div className="app-company-logo">
     90                        <img
     91                            // loading gif
     92                            src={logosState[application.recruiterId]}
     93                            alt=""
     94                            width={75} height={75}
     95                        />
     96                    </div>
     97
     98                    <div className="app-info">
     99                        <div className="jobAd-title">{application.jobAdTitle}</div>
    52100                        <div className="contact-info">
    53101                            <div className="contact-item">
     
    68116                    </div>
    69117
    70                     <div className="right-box">
    71                         <ViewApplicationDetails application={application}/>
    72                         <div className="select">
    73                             <Select isDisabled={true} options={options} defaultValue={handleDefaultValue(application.status)}/>
    74                         </div>
     118                    <div className="app-status">
     119                        <ApplicationDetailsModal application={application}/>
     120                        <> {handleDefaultValue(application.status).label}</>
     121                        {/*<div className="select">*/}
     122                        {/*    <Select isDisabled={true} options={options} />*/}
     123                        {/*</div>*/}
    75124
    76125                    </div>
  • jobvista-frontend/src/views/applications/ApplyToJobAdModal.js

    rb248810 rbefb988  
    11import React, {useState} from "react";
    2 import "../job_advertisements/Form.css";
     2import "../shared_css/Modal.css";
    33
    44import 'react-responsive-modal/styles.css';
    55import {Modal} from 'react-responsive-modal';
    6 import Select from "react-select";
    76
    87//Validation
     
    1211
    1312
    14 import {employmentStatusOptions, industryOptions, jobTypeOptions} from "../selectOptions";
    1513import {useDispatch, useSelector} from "react-redux";
    16 import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
    1714import Roles from "../../enumerations/Roles";
    1815import {ApplicationActions} from "../../redux/actions/applicationActions";
     16import {notifyJobAdApply} from "../../utils/toastUtils";
    1917
    2018
     
    5553                formData,(success, response) => {
    5654                    if(success) {
    57                         console.log("Job Advertisement added")
    5855                        toggleModal()
     56                        notifyJobAdApply()
    5957                    }
    6058                }
     
    7068        {role===Roles.JOBSEEKER &&
    7169            <>
    72                 {jobAd.active && <button onClick={toggleModal} className="card-button apply">Apply now</button> }
     70                {jobAd.active && <button onClick={toggleModal} className="apply-button apply">Apply now</button> }
    7371                {!jobAd.active && <button className="card-button apply disabled">Apply now</button> }
    7472            </>
     
    8684                            <label className="label">Why are you interested in joining our company?</label>
    8785                            <textarea type="text" placeholder="Write your answer here..."
    88                                       {...register("answerOne")} className="applictaion-textarea"/>
     86                                      {...register("answerOne")} className="application-textarea"/>
    8987                            <p style={{color: "red"}}>{errors.answerOne?.message}</p>
    9088
    9189                            <label className="label">What makes you a good fit for this position?</label>
    9290                            <textarea type="text" placeholder="Write your answer here..."
    93                                       {...register("answerTwo")} className="applictaion-textarea"/>
     91                                      {...register("answerTwo")} className="application-textarea"/>
    9492                            <p style={{color: "red"}}>{errors.answerTwo?.message}</p>
    9593
    9694                            <label className="label">What do you hope to achieve in your first 6 months in this role?</label>
    9795                            <textarea type="text"  placeholder="Write your answer here..."
    98                                    {...register("answerThree")} className="applictaion-textarea"/>
     96                                   {...register("answerThree")} className="application-textarea"/>
    9997                            <p style={{color: "red"}}>{errors.answerThree?.message}</p>
    10098
     
    103101                            <label htmlFor="start">Curriculum vitae (CV)</label>
    104102                            <br/>
    105                             <input {...register("file")} onChange={(e) => setResumeFile(e.target.files[0])} required type="file" id="fileUpload" accept=".pdf"/>
     103                            <input {...register("file")} className="resume-link" onChange={(e) => setResumeFile(e.target.files[0])} required type="file" id="fileUpload" accept=".pdf"/>
    106104
    107105                            <br/>
    108106                            <label className="label">Message to the recruiter</label>
    109107                            <textarea type="text" placeholder="Optional..."
    110                                       {...register("messageToRecruiter")} className="applictaion-textarea"/>
     108                                      {...register("messageToRecruiter")} className="application-textarea"/>
    111109
    112110                            <br/><br/>
     
    115113                    </div>
    116114
    117                     <div className="aligned">
     115                    <div className="modal-buttons">
     116                        <div className="cancel-btn" onClick={toggleModal}> Cancel</div>
    118117                        <button className="submit-btn"> Submit</button>
    119118                    </div>
Note: See TracChangeset for help on using the changeset viewer.