Changeset befb988


Ignore:
Timestamp:
06/17/24 21:59:14 (3 months 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.

Files:
1 deleted
46 edited
3 moved

Legend:

Unmodified
Added
Removed
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/config/SecurityConfiguration.java

    rb248810 rbefb988  
    22
    33import lombok.RequiredArgsConstructor;
     4import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role;
    45import org.springframework.context.annotation.Bean;
    56import org.springframework.context.annotation.Configuration;
     
    3132                .authorizeHttpRequests(request -> request
    3233                        // TO DO: FIX PERMISSIONS
    33                         .requestMatchers("/api/job-advertisements/**","/api/job-advertisements/view/**","/api/recruiter/info/**",
    34                                 "/api/job-advertisements/apply/**","/api/auth/**", "/api/resume/**", "/api/my-applications/**", "/api/applications/{id}/update", "/api/admin/**").permitAll()
    35                         //.requestMatchers("/api/job-advertisements/**").hasAnyAuthority(Role.ROLE_RECRUITER.name())
     34                        .requestMatchers("/api/job-advertisements/**",
     35                                "/api/job-advertisements/view/**",
     36                                "/api/recruiter/**",
     37                                "/api/job-seeker/**",
     38                                "/api/recruiter/{id}/info",
     39                                "/api/recruiter/{id}/edit-info",
     40                                "/api/job-advertisements/apply/**",
     41                                "/api/auth/**",
     42                                "/api/resume/**",
     43                                "/api/my-applications/**",
     44                                "/api/applications/{id}/update",
     45                                "/api/admin/**").permitAll()
     46//                        .requestMatchers("/api/recruiter").hasAnyAuthority(Role.ROLE_RECRUITER.name())
    3647                        .anyRequest().authenticated())
    3748                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/AdminController.java

    rb248810 rbefb988  
    22
    33import lombok.RequiredArgsConstructor;
     4import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO;
    45import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
    56import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AdminService;
     
    2021    @PostMapping("/change-access/{recruiter_id}")
    2122    public ResponseEntity<?> changeAccess(@PathVariable("recruiter_id") Long recruiterId, @RequestBody boolean access) {
    22         RecruiterDetailsDTO recruiterDetailsDTO = adminService.changeAccess(recruiterId, access);
    23         return new ResponseEntity<>(recruiterDetailsDTO, HttpStatus.OK);
     23        RecruiterAdminDetailsDTO recruiterAdminDetailsDTO = adminService.changeAccess(recruiterId, access);
     24        return new ResponseEntity<>(recruiterAdminDetailsDTO, HttpStatus.OK);
    2425    }
    2526
    2627    @GetMapping("/recruiters")
    2728    public ResponseEntity<?> findAllRecruiters() {
    28         List<RecruiterDetailsDTO> recruiterDetailsDTOList = adminService.findAllRecruiters();
    29         return new ResponseEntity<>(recruiterDetailsDTOList, HttpStatus.OK);
     29        List<RecruiterAdminDetailsDTO> recruiterAdminDetailsDTOList = adminService.findAllRecruiters();
     30        return new ResponseEntity<>(recruiterAdminDetailsDTOList, HttpStatus.OK);
    3031    }
    3132}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/JobSeekerController.java

    rb248810 rbefb988  
    33
    44import lombok.AllArgsConstructor;
    5 import org.springframework.web.bind.annotation.CrossOrigin;
    6 import org.springframework.web.bind.annotation.RequestMapping;
    7 import org.springframework.web.bind.annotation.RestController;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerEditDetailsDTO;
     6import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
     7import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JobSeekerService;
     8import org.springframework.core.io.Resource;
     9import org.springframework.http.HttpHeaders;
     10import org.springframework.http.HttpStatus;
     11import org.springframework.http.MediaType;
     12import org.springframework.http.ResponseEntity;
     13import org.springframework.web.bind.annotation.*;
     14import org.springframework.web.multipart.MultipartFile;
    815
    916@RestController
     
    1219@CrossOrigin(origins = "*")
    1320public class JobSeekerController {
     21    private final JobSeekerService jobSeekerService;
     22
     23    @GetMapping("/{id}/edit-info")
     24    public ResponseEntity<?> getJobSeekerEditDetailsById(@PathVariable("id") Long id) {
     25        JobSeekerEditDetailsDTO jobSeekerEditDetailsDTO = jobSeekerService.getJobSeekerEditDetailsById(id);
     26        return new ResponseEntity<>(jobSeekerEditDetailsDTO, HttpStatus.OK);
     27    }
     28
     29    @PostMapping("/{id}/edit-info")
     30    public ResponseEntity<?> editRecruiterDetailsById(@PathVariable("id") Long id, @RequestBody JobSeekerEditDetailsDTO jobSeekerEditDetailsDTO) {
     31        JobSeekerEditDetailsDTO jobSeekerEditDetailsDTOresp = jobSeekerService.editJobSeekerDetailsById(id, jobSeekerEditDetailsDTO);
     32        return new ResponseEntity<>(jobSeekerEditDetailsDTOresp, HttpStatus.OK);
     33    }
     34
     35    @PostMapping("/submit-profile-pic")
     36    public ResponseEntity<?> submitJobSeekerProfilePic(
     37            @RequestParam("jobSeekerId") Long jobSeekerId,
     38            @RequestParam("profilePicFile") MultipartFile profilePicFile) {
     39        jobSeekerService.submitProfilePic(jobSeekerId, profilePicFile);
     40        return new ResponseEntity<>(HttpStatus.OK);
     41    }
     42
     43    @GetMapping("/{id}/download-profile-pic")
     44    public ResponseEntity<?> downloadJobSeekerProfilePic(@PathVariable("id") Long id) {
     45        Resource resource = jobSeekerService.loadProfilePicAsResource(id);
     46        return ResponseEntity.ok()
     47                .contentType(MediaType.IMAGE_JPEG)
     48                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
     49                .body(resource);
     50    }
    1451}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/RecruiterController.java

    rb248810 rbefb988  
    33import lombok.AllArgsConstructor;
    44import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
    56import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.RecruiterService;
     7import org.springframework.core.io.Resource;
     8import org.springframework.http.HttpHeaders;
    69import org.springframework.http.HttpStatus;
     10import org.springframework.http.MediaType;
    711import org.springframework.http.ResponseEntity;
    812import org.springframework.web.bind.annotation.*;
     13import org.springframework.web.multipart.MultipartFile;
    914
    1015@RestController
     
    1621    private final RecruiterService recruiterService;
    1722
    18     @GetMapping("/info/{id}")
     23    @GetMapping("/{id}/info")
    1924    public ResponseEntity<?> getRecruiterDetailsById(@PathVariable("id") Long id) {
    2025        RecruiterDetailsDTO recruiterDetailsDTO = recruiterService.getRecruiterDetailsById(id);
    2126        return new ResponseEntity<>(recruiterDetailsDTO, HttpStatus.OK);
    2227    }
     28
     29    @GetMapping("/{id}/edit-info")
     30    public ResponseEntity<?> getRecruiterEditDetailsById(@PathVariable("id") Long id) {
     31        RecruiterEditDetailsDTO recruiterEditDetailsDTO = recruiterService.getRecruiterEditDetailsById(id);
     32        return new ResponseEntity<>(recruiterEditDetailsDTO, HttpStatus.OK);
     33    }
     34
     35    @PostMapping("/{id}/edit-info")
     36    public ResponseEntity<?> editRecruiterDetailsById(@PathVariable("id") Long id, @RequestBody RecruiterEditDetailsDTO recruiterEditDetailsDTO) {
     37        RecruiterEditDetailsDTO recruiterEditDetailsDTOresp = recruiterService.editRecruiterDetailsById(id, recruiterEditDetailsDTO);
     38        return new ResponseEntity<>(recruiterEditDetailsDTOresp, HttpStatus.OK);
     39    }
     40
     41    @PostMapping("/submit-logo")
     42    public ResponseEntity<?> submitRecruiterLogo(
     43            @RequestParam("recruiterId") Long recruiterId,
     44            @RequestParam("logoFile") MultipartFile logoFile) {
     45        recruiterService.submitLogo(recruiterId, logoFile);
     46        return new ResponseEntity<>(HttpStatus.OK);
     47    }
     48
     49    @GetMapping("/{id}/download-logo")
     50    public ResponseEntity<?> downloadRecruiterLogo(@PathVariable("id") Long id) {
     51        Resource resource = recruiterService.loadLogoAsResource(id);
     52        return ResponseEntity.ok()
     53                .contentType(MediaType.IMAGE_JPEG)
     54                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
     55                .body(resource);
     56    }
    2357}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/Application.java

    rb248810 rbefb988  
    6262                application.getJobSeeker().getEmail(),
    6363                application.getJobSeeker().getPhoneNumber(),
     64                application.getJobAdvertisement().getRecruiter().getId(),
    6465                application.getJobAdvertisement().getRecruiter().getName(),
    65                 application.getJobAdvertisement().getRecruiter().getEmail(),
    66                 application.getJobAdvertisement().getRecruiter().getPhoneNumber(),
     66                application.getJobAdvertisement().getRecruiter().getContactEmailAddress(),
     67                application.getJobAdvertisement().getRecruiter().getContactPhoneNumber(),
    6768                application.getJobAdvertisement().getId(),
    6869                application.getJobAdvertisement().getTitle(),
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationDetailsDTO.java

    rb248810 rbefb988  
    1818    private String jobSeekerEmail;
    1919    private String jobSeekerPhoneNumber;
     20    private Long recruiterId;
    2021    private String recruiterName;
    2122    private String recruiterEmail;
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/job_advertisements/DTO/JobAdvertisementDTO.java

    rb248810 rbefb988  
    1111@NoArgsConstructor
    1212public class JobAdvertisementDTO {
    13     private String email;
     13    private Long id;
    1414    private String title;
    1515    private String description;
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/DTO/RecruiterDetailsDTO.java

    rb248810 rbefb988  
    1111@NoArgsConstructor
    1212public class RecruiterDetailsDTO {
    13     private Long id;
    14     private String email;
    1513    private String companyName;
    1614    private String companyDescription;
    17     private String phoneNumber;
    18     private boolean hasAccess;
    19     private LocalDateTime registeredOn;
     15    private String contactEmail;
     16    private String contactPhoneNumber;
     17    private String receptionist;
    2018}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/JobSeeker.java

    rb248810 rbefb988  
    88import lombok.NoArgsConstructor;
    99import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role;
     10
     11import java.nio.file.Paths;
    1012
    1113@Entity
     
    2224        this.lastName = lastName;
    2325        this.phoneNumber = phoneNumber;
     26
     27        String relativeProfilePicFilePath = Paths.get("uploads", "job-seekers", "profile-pics", "default-profile-pic.png").toString();
     28        this.profilePicFilePath = relativeProfilePicFilePath;
     29
    2430        this.role = Role.ROLE_JOBSEEKER;
    2531    }
     
    3440    private String phoneNumber;
    3541
     42    private String profilePicFilePath;
     43
    3644    @Override
    3745    public String getName() {
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/Recruiter.java

    rb248810 rbefb988  
    99import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role;
    1010
     11import java.nio.file.Paths;
     12
    1113@Entity
    1214@Data
    1315@NoArgsConstructor
    14 @AllArgsConstructor
    1516@Table(name = "recruiters")
    1617public class Recruiter extends User {
     
    2122        this.companyName = companyName;
    2223        this.companyDescription = "";
    23         this.phoneNumber = phoneNumber;
     24        this.contactEmailAddress = email;
     25        this.contactPhoneNumber = phoneNumber;
     26        this.receptionist = "";
     27
     28        String relativeLogoFilePath = Paths.get("uploads", "logo", "default-company-logo.png").toString();
     29        this.logoFilePath = relativeLogoFilePath;
    2430        this.role = Role.ROLE_RECRUITER;
    2531    }
     
    3137    private String companyDescription;
    3238
    33     @Column(name = "phone_number")
    34     private String phoneNumber;
     39    private String contactEmailAddress;
     40
     41    private String contactPhoneNumber;
     42
     43    private String receptionist;
     44
     45    private String logoFilePath;
    3546
    3647    @Override
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/mappers/JobSeekerMapper.java

    rb248810 rbefb988  
    22
    33import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerDTO;
     4import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerEditDetailsDTO;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
    46import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker;
     7import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
    58
    69public class JobSeekerMapper {
     
    2629    }
    2730
     31    public static JobSeekerEditDetailsDTO mapToJobSeekerEditDetailsDTO(JobSeeker jobSeeker) {
     32        return new JobSeekerEditDetailsDTO(
     33                jobSeeker.getEmail(),
     34                jobSeeker.getFirstName(),
     35                jobSeeker.getLastName(),
     36                jobSeeker.getPhoneNumber()
     37        );
     38    }
     39
    2840}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/mappers/RecruiterMapper.java

    rb248810 rbefb988  
    11package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers;
    22
     3import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO;
    34import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDTO;
    45import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
     6import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
    57import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
    68
     
    1214                recruiter.getPassword(),
    1315                recruiter.getCompanyName(),
    14                 recruiter.getPhoneNumber()
     16                recruiter.getContactPhoneNumber()
    1517        );
    1618    }
     
    1820    public static RecruiterDetailsDTO mapToRecruiterDetailsDTO(Recruiter recruiter) {
    1921        return new RecruiterDetailsDTO(
     22                recruiter.getCompanyName(),
     23                recruiter.getCompanyDescription(),
     24                recruiter.getContactEmailAddress(),
     25                recruiter.getContactPhoneNumber(),
     26                recruiter.getReceptionist()
     27        );
     28    }
     29    public static RecruiterEditDetailsDTO mapToRecruiterEditDetailsDTO(Recruiter recruiter) {
     30        return new RecruiterEditDetailsDTO(
     31                recruiter.getEmail(),
     32                recruiter.getCompanyName(),
     33                recruiter.getCompanyDescription(),
     34                recruiter.getContactEmailAddress(),
     35                recruiter.getContactPhoneNumber(),
     36                recruiter.getReceptionist()
     37        );
     38    }
     39
     40    public static RecruiterAdminDetailsDTO mapToRecruiterAdminDetailsDTO(Recruiter recruiter) {
     41        return new RecruiterAdminDetailsDTO(
    2042                recruiter.getId(),
    2143                recruiter.getEmail(),
    2244                recruiter.getCompanyName(),
    2345                recruiter.getCompanyDescription(),
    24                 recruiter.getPhoneNumber(),
     46                recruiter.getContactEmailAddress(),
     47                recruiter.getContactPhoneNumber(),
     48                recruiter.getReceptionist(),
    2549                recruiter.isHasAccess(),
    2650                recruiter.getRegisteredOn()
     
    3660        );
    3761    }
     62
    3863//    Using MapStruct:
    3964//    RecruiterMapper INSTANCE = Mappers.getMapper(RecruiterMapper.class);
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/AdminServiceImpl.java

    rb248810 rbefb988  
    33import lombok.AllArgsConstructor;
    44import lombok.RequiredArgsConstructor;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO;
    56import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
    67import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
     
    2021
    2122    @Override
    22     public RecruiterDetailsDTO changeAccess(long recruiterId, boolean access) {
     23    public RecruiterAdminDetailsDTO changeAccess(long recruiterId, boolean access) {
    2324        Recruiter recruiter = recruiterRepository.findById(recruiterId).orElse(null);
    2425        if (recruiter != null) {
    2526            recruiter.setHasAccess(access);
    2627            recruiterRepository.save(recruiter);
    27             return RecruiterMapper.mapToRecruiterDetailsDTO(recruiter);
     28            return RecruiterMapper.mapToRecruiterAdminDetailsDTO(recruiter);
    2829        }
    2930        return null;
     
    3233
    3334    @Override
    34     public List<RecruiterDetailsDTO> findAllRecruiters() {
     35    public List<RecruiterAdminDetailsDTO> findAllRecruiters() {
    3536        List<Recruiter> recruiterList = recruiterRepository.findAll();
    36         return recruiterList.stream().map(RecruiterMapper::mapToRecruiterDetailsDTO).toList();
     37        return recruiterList.stream().map(RecruiterMapper::mapToRecruiterAdminDetailsDTO).toList();
    3738    }
    3839}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/JobAdvertisementServiceImpl.java

    rb248810 rbefb988  
    3030    @Override
    3131    public JobAdDetailsDTO addJobAdvertisement(JobAdvertisementDTO jobAdvertisementDTO) {
    32         Recruiter recruiter = recruiterRepository.findRecruiterByEmail(jobAdvertisementDTO.getEmail()).orElseThrow(() -> new IllegalArgumentException("User not found"));
     32        Recruiter recruiter = recruiterRepository.findById(jobAdvertisementDTO.getId()).orElseThrow(() -> new IllegalArgumentException("User not found"));
    3333        JobAdvertisement jobAdvertisement = new JobAdvertisement(
    3434                recruiter,
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/JwtServiceImpl.java

    rb248810 rbefb988  
    2525                .claim("role", user.getRole())
    2626                .claim("access", user.isHasAccess())
     27                .claim("id", user.getId())
    2728                .setIssuedAt(new Date())
    2829                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/RecruiterServiceImpl.java

    rb248810 rbefb988  
    33import lombok.RequiredArgsConstructor;
    44import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
     5import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
    56import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
    67import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.RecruiterMapper;
    78import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.RecruiterRepository;
    89import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.RecruiterService;
     10import org.springframework.beans.factory.annotation.Autowired;
     11import org.springframework.beans.factory.annotation.Value;
     12import org.springframework.core.io.Resource;
     13import org.springframework.core.io.UrlResource;
    914import org.springframework.stereotype.Service;
     15import org.springframework.web.multipart.MultipartFile;
     16
     17import java.io.IOException;
     18import java.nio.file.Files;
     19import java.nio.file.Path;
     20import java.nio.file.Paths;
     21import java.nio.file.StandardCopyOption;
    1022
    1123@Service
     
    1426
    1527    private final RecruiterRepository recruiterRepository;
     28    private final Path logoStorageLocation;
     29
     30    @Autowired
     31    RecruiterServiceImpl(@Value("./uploads") String uploadDir, RecruiterRepository recruiterRepository) {
     32        this.recruiterRepository = recruiterRepository;
     33
     34        this.logoStorageLocation = Paths.get(uploadDir + "/logo").toAbsolutePath().normalize();
     35        try {
     36            Files.createDirectories(this.logoStorageLocation);
     37        } catch (IOException ex) {
     38            throw new RuntimeException("Could not create the directory where the uploaded files will be stored.", ex);
     39        }
     40    }
     41
     42
     43    @Override
     44    public RecruiterEditDetailsDTO editRecruiterDetailsById(Long recruiterId, RecruiterEditDetailsDTO recruiterEditDetailsDTO) {
     45        Recruiter recruiter = recruiterRepository.findById(recruiterId).orElse(null);
     46        recruiter.setEmail(recruiterEditDetailsDTO.getEmail());
     47        recruiter.setCompanyName(recruiterEditDetailsDTO.getCompanyName());
     48        recruiter.setCompanyDescription(recruiterEditDetailsDTO.getCompanyDescription());
     49        recruiter.setContactEmailAddress(recruiterEditDetailsDTO.getContactEmail());
     50        recruiter.setContactPhoneNumber(recruiterEditDetailsDTO.getContactPhoneNumber());
     51        recruiter.setReceptionist(recruiterEditDetailsDTO.getReceptionist());
     52        recruiterRepository.save(recruiter);
     53        return RecruiterMapper.mapToRecruiterEditDetailsDTO(recruiter);
     54    }
     55
     56    @Override
     57    public RecruiterEditDetailsDTO getRecruiterEditDetailsById(Long recruiterId) {
     58        Recruiter recruiter = recruiterRepository.findById(recruiterId).orElse(null);
     59        return RecruiterMapper.mapToRecruiterEditDetailsDTO(recruiter);
     60    }
     61
    1662
    1763    @Override
     
    2066        return RecruiterMapper.mapToRecruiterDetailsDTO(recruiter);
    2167    }
     68
     69    @Override
     70    public void submitLogo(Long recruiterId, MultipartFile logoFile) {
     71
     72        Path recruiterLogoDir = this.logoStorageLocation.resolve(String.valueOf(recruiterId));
     73        try {
     74            Files.createDirectories(recruiterLogoDir);
     75            String originalFilename = logoFile.getOriginalFilename();
     76
     77            if (originalFilename != null) {
     78                Path targetLocation = recruiterLogoDir.resolve(originalFilename);
     79
     80                Files.copy(logoFile.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
     81
     82                Recruiter recruiter = recruiterRepository.findById(recruiterId)
     83                        .orElseThrow(() -> new RuntimeException("Recruiter not found"));
     84                String relativePath = Paths.get("uploads", "logo", String.valueOf(recruiterId), originalFilename).toString();
     85                recruiter.setLogoFilePath(relativePath);
     86                recruiterRepository.save(recruiter);
     87            }
     88
     89        } catch (IOException ex) {
     90            throw new RuntimeException("Could not store file. Please try again!", ex);
     91        }
     92
     93     
     94    }
     95
     96    public Resource loadLogoAsResource(Long recruiterId) {
     97        Recruiter recruiter = recruiterRepository.findById(recruiterId)
     98                .orElseThrow(() -> new RuntimeException("Recruiter not found"));
     99
     100        try {
     101            String relativeLogoPath = recruiter.getLogoFilePath();
     102            Path logoPath = this.logoStorageLocation.getParent().getParent().resolve(relativeLogoPath);
     103            Resource resource = new UrlResource(logoPath.toUri());
     104            if (resource.exists()) {
     105                return resource;
     106            } else {
     107                throw new RuntimeException("File not found " + logoPath);
     108            }
     109        } catch (IOException ex) {
     110            throw new RuntimeException("File not found " + ex);
     111        }
     112
     113    }
     114
    22115}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/AdminService.java

    rb248810 rbefb988  
    11package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef;
    22
     3import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO;
    34import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
    45
     
    67
    78public interface AdminService {
    8     RecruiterDetailsDTO changeAccess(long recruiterId, boolean access);
    9     List<RecruiterDetailsDTO> findAllRecruiters();
     9    RecruiterAdminDetailsDTO changeAccess(long recruiterId, boolean access);
     10    List<RecruiterAdminDetailsDTO> findAllRecruiters();
    1011}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/RecruiterService.java

    rb248810 rbefb988  
    22
    33import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO;
    4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
     4import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO;
     5import org.springframework.core.io.Resource;
     6import org.springframework.web.multipart.MultipartFile;
    57
    68public interface RecruiterService {
    79    RecruiterDetailsDTO getRecruiterDetailsById(Long recruiterId);
     10    RecruiterEditDetailsDTO editRecruiterDetailsById(Long recruiterId, RecruiterEditDetailsDTO recruiterEditDetailsDTO);
     11    RecruiterEditDetailsDTO getRecruiterEditDetailsById(Long recruiterId);
     12
     13    void submitLogo(Long recruiterId, MultipartFile logoFile);
     14    Resource loadLogoAsResource(Long recruiterId);
    815}
  • jobvista-frontend/package-lock.json

    rb248810 rbefb988  
    3232        "react-scripts": "5.0.1",
    3333        "react-select": "^5.8.0",
     34        "react-toastify": "^10.0.5",
    3435        "redux": "^5.0.1",
    3536        "redux-thunk": "^3.1.0",
     
    1602716028      }
    1602816029    },
     16030    "node_modules/react-toastify": {
     16031      "version": "10.0.5",
     16032      "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
     16033      "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
     16034      "dependencies": {
     16035        "clsx": "^2.1.0"
     16036      },
     16037      "peerDependencies": {
     16038        "react": ">=18",
     16039        "react-dom": ">=18"
     16040      }
     16041    },
    1602916042    "node_modules/react-transition-group": {
    1603016043      "version": "4.4.5",
  • jobvista-frontend/package.json

    rb248810 rbefb988  
    2727    "react-scripts": "5.0.1",
    2828    "react-select": "^5.8.0",
     29    "react-toastify": "^10.0.5",
    2930    "redux": "^5.0.1",
    3031    "redux-thunk": "^3.1.0",
  • jobvista-frontend/src/App.css

    rb248810 rbefb988  
    22@import url('https://fonts.googleapis.com/css2?family=Cabin:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap');
    33
     4* {
     5  margin: 0;
     6  padding: 0;
     7  box-sizing: border-box;
     8}
     9
     10html {
     11  scroll-behavior: smooth;
     12}
     13
    414.App {
    5   background-color: rgb(243, 242, 241);
    6   //background-color: #EBF2FC;
     15  /*background-color: rgb(243, 242, 241);*/
     16  background-color: #F5F5F5;
     17  /*background-color: #F4F2EE;*/
     18  /*background-color: #0F0F0F;*/
     19  /*background-color: #EBF2FC;*/
    720  height: 100vh;
    821  overflow-y: auto;
     
    1427  -webkit-box-pack: center;
    1528  justify-content: center;
    16 margin-bottom: 20px;
     29  margin-bottom: 20px;
    1730  margin-top: 20px;
    1831}
     
    3649}
    3750
     51.container, .custom-container {
     52  margin-top: 120px !important;
     53}
     54
     55.no-additional-margin {
     56  margin-top: 80px !important;
     57}
     58
    3859
    3960/*font-family: 'Ubuntu', sans-serif;*/
     
    4970  height: 280px !important;
    5071}
     72
     73.container .g-4 {
     74  --bs-gutter-y: 6rem !important;
     75  margin-bottom: 100px !important;
     76}
     77
    5178
    5279.add-new-card {
     
    80107  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    81108  transform: translate(0, 0);
    82   height: 260px;
     109  height: auto;
    83110}
    84111
     
    167194  border-radius: 8px;
    168195  width: 45%;
    169   background-color: rgba(207, 235, 255, 1);
    170   //background-size: 200% auto;
    171   //background-image: linear-gradient(to right, #a1c4fd 0%, aliceblue 61%, #a1c4fd 100%);
    172   color: black;
     196  background-image: linear-gradient(to bottom, #dddddd, #f0f0f0);
     197  color: #333333;
    173198  font-weight: bold;
    174199  padding: 5px 10px;
    175200  transition: 0.2s;
     201  text-decoration: none;
     202  text-align: center;
    176203}
    177204
     
    180207}
    181208.card-button:not(.disabled):hover{
    182   background-color: rgb(187, 215, 235);
    183   //background-position: right center;
    184   color: black;
    185 }
    186 
    187 .disabled {
    188   cursor: default !important;
    189   opacity: 0.6;
    190 }
    191 
    192 
    193 
     209  background: linear-gradient(to bottom, #bbbbbb, #dddddd);
     210  color: #333333;
     211}
     212
     213
     214
     215
     216
  • jobvista-frontend/src/App.js

    rb248810 rbefb988  
    44import {BrowserRouter} from "react-router-dom";
    55import {Header} from "./views/static/Header";
    6 import {RoutesConfig} from "./auth/RoutesConfig";
    7 import {useEffect, useState} from "react";
     6import RoutesConfig from "./auth/RoutesConfig";
     7import React, {useEffect, useState} from "react";
    88import {AuthActions} from "./redux/actions/authActions";
    99import {AUTH_TOKEN} from "./axios/axiosInstance";
    1010import {jwtDecode} from "jwt-decode";
    1111import {NoAccess} from "./views/static/NoAccess";
     12import {Loading} from "./views/static/Loading";
     13import {ToastContainer} from "react-toastify";
    1214
    1315function App() {
     
    2022    const [user, setUser] = useState(null);
    2123    const [loading, setLoading] = useState(true);
     24    const [minimumLoadingTime, setMinimumLoadingTime] = useState(true);
     25
    2226    const auth = useSelector(state => state.auth);
     27
     28    useEffect(() => {
     29        // Simulate a minimum loading time of 1 second
     30        const timer = setTimeout(() => {
     31            setMinimumLoadingTime(false);
     32        }, 1000);
     33
     34        // Clear timeout if component unmounts
     35        return () => clearTimeout(timer);
     36    }, []);
    2337
    2438    useEffect(() => {
     
    3044                    name: decodedToken.name,
    3145                    role: decodedToken.role,
    32                     hasAccess: auth.currentUser.access,
     46                    hasAccess: decodedToken.access,
     47                    id: decodedToken.id
    3348                });
    3449                setLoading(false);
     
    4257    }, [auth]);
    4358
    44     if (loading) {
    45         return <NoAccess />; // Replace LoadingSpinner with your loading indicator component
     59    if (loading || minimumLoadingTime) {
     60        return <Loading />; // Replace LoadingSpinner with your loading indicator component
    4661    }
    4762
     
    5873                      <Header />
    5974                      <RoutesConfig />
     75                      {/*<Loading/>*/}
    6076                  </>
    6177              ) : (
    6278                  <NoAccess user={user}/>
    6379              )}
    64 
    65 
    6680          </BrowserRouter>
     81          <ToastContainer/>
    6782      </div>
    6883  );
  • jobvista-frontend/src/auth/RoutesConfig.js

    rb248810 rbefb988  
    1 
    2 import {Route, Router, Routes} from 'react-router-dom'
     1import {Navigate, Route, Router, Routes} from 'react-router-dom'
    32import {Dashboard} from "../views/dashboard/Dashboard";
    43import {SignInForm} from "../views/auth/SignInForm";
    54import {SignUpRecruiterForm} from "../views/auth/SignUpRecruiterForm";
    65import {SignUpJobSeekerForm} from "../views/auth/SignUpJobSeekerForm";
    7 import {JobAdvertisements} from "../views/job_advertisements/JobAdvertisements";
     6import {Workspace} from "../views/job_advertisements/RecruiterWorkspace";
    87import {JobAdDetails} from "../views/job_advertisements/JobAdDetails";
    98import {ApplicationsByJobAd} from "../views/applications/ApplicationsByJobAd";
    109import {ApplicationsByJobSeeker} from "../views/applications/ApplicationsByJobSeeker";
     10
     11import {AdminPanel} from "../views/admin_panel/AdminPanel";
     12import {RecruiterProfile} from "../views/recruiters/RecruiterProfile";
     13import {AboutUs} from "../views/static/AboutUs";
     14import {JobSeekerEditProfile} from "../views/edit_profile/JobSeekerEditProfile";
     15import {RecruiterEditProfile} from "../views/edit_profile/RecruiterEditProfile";
     16import Roles from "../enumerations/Roles";
     17import {useSelector} from "react-redux";
    1118import {useEffect, useState} from "react";
    12 import {AUTH_TOKEN} from "../axios/axiosInstance";
    13 import {jwtDecode} from "jwt-decode";
    14 import {useSelector} from "react-redux";
    15 import {AdminPanel} from "../views/admin_panel/AdminPanel";
    16 export const RoutesConfig = () => {
     19import {ErrorPage} from "../views/static/ErrorPage";
     20
     21export const PrivateRoutes = [
     22    {
     23        component: AdminPanel,
     24        path: '/admin-panel',
     25        title: 'Admin Panel',
     26        exact: true,
     27        permission: [
     28            Roles.ADMIN
     29        ]
     30    },
     31    {
     32        component: Workspace,
     33        path: '/job-management-hub',
     34        title: 'Job Management Hub',
     35        exact: true,
     36        permission: [
     37            Roles.RECRUITER,
     38        ]
     39    },
     40    {
     41        component: ApplicationsByJobAd,
     42        path: '/job-management-hub/applications/:advertisement_id',
     43        title: 'Applications by Job Ad',
     44        exact: true,
     45        permission: [
     46            Roles.RECRUITER
     47        ]
     48    },
     49    {
     50        component: ApplicationsByJobSeeker,
     51        path: '/my-applications',
     52        title: 'My Applications',
     53        exact: true,
     54        permission: [
     55            Roles.JOBSEEKER
     56        ]
     57    },
     58
     59    {
     60        component: JobSeekerEditProfile,
     61        path: '/job-seeker/edit-profile',
     62        title: 'Edit Job Seeker Profile',
     63        exact: true,
     64        permission: [
     65            Roles.JOBSEEKER
     66        ]
     67    },
     68    {
     69        component: RecruiterEditProfile,
     70        path: '/recruiter/edit-profile',
     71        title: 'Edit Recruiter Profile',
     72        exact: true,
     73        permission: [
     74            Roles.RECRUITER
     75        ]
     76    },
     77]
     78
     79export const PublicRoutes = [
     80    {
     81        component: Dashboard,
     82        path: "/",
     83        exact: true,
     84        permission: [
     85            Roles.RECRUITER,
     86            Roles.JOBSEEKER,
     87            Roles.ADMIN,
     88            Roles.GUEST
     89        ]
     90    },
     91    {
     92        component: SignInForm,
     93        path: '/signin',
     94        title: 'Sign In',
     95        exact: true,
     96        permission: [
     97            Roles.GUEST,
     98            Roles.RECRUITER,
     99            Roles.JOBSEEKER,
     100            Roles.ADMIN
     101        ]
     102    },
     103
     104    {
     105        component: SignUpRecruiterForm,
     106        path: '/signup/recruiter',
     107        title: 'Sign Up as Recruiter',
     108        permission: [
     109            Roles.GUEST,
     110            Roles.RECRUITER,
     111            Roles.JOBSEEKER,
     112            Roles.ADMIN
     113        ]
     114    },
     115    {
     116        component: SignUpJobSeekerForm,
     117        path: '/signup/job-seeker',
     118        title: 'Sign Up as Job Seeker',
     119        permission: [
     120            Roles.GUEST,
     121            Roles.RECRUITER,
     122            Roles.JOBSEEKER,
     123            Roles.ADMIN
     124        ]
     125    },
     126    {
     127        component: JobAdDetails,
     128        path: '/job-advertisements/:id',
     129        title: 'Job Advertisement Details',
     130        exact: true,
     131        permission: [
     132            Roles.RECRUITER,
     133            Roles.JOBSEEKER,
     134            Roles.ADMIN,
     135            Roles.GUEST
     136        ]
     137    },
     138    {
     139        component: RecruiterProfile,
     140        path: '/recruiters/:id',
     141        title: 'Recruiter Profile',
     142        exact: true,
     143        permission: [
     144            Roles.RECRUITER,
     145            Roles.JOBSEEKER,
     146            Roles.ADMIN,
     147            Roles.GUEST
     148        ]
     149    },
     150    {
     151        component: AboutUs,
     152        path: '/about',
     153        title: 'About Us',
     154        exact: true,
     155        permission: [
     156            Roles.GUEST,
     157            Roles.RECRUITER,
     158            Roles.JOBSEEKER,
     159            Roles.ADMIN
     160        ]
     161    },
     162]
     163
     164const AllRoutes = [...PrivateRoutes, ...PublicRoutes];
     165
     166const filterRoutes = (roleParam) => {
     167    return AllRoutes.filter(route => {
     168        return route.permission.includes(roleParam);
     169    });
     170};
     171const RoutesConfig = () => {
     172    const currentUser = useSelector(state => state.auth.currentUser);
     173    const [role, setRole] = useState(Roles.GUEST);
     174
     175    useEffect(() => {
     176        if (currentUser) {
     177            setRole(currentUser.role);
     178        }
     179    }, [currentUser]);
    17180
    18181    return (
    19             <Routes>
    20                 <Route path="/" element={<Dashboard/>}></Route>
    21                 <Route path="/signin" element={<SignInForm/>}></Route>
    22                 <Route path="/signup/recruiter" element={<SignUpRecruiterForm/>}></Route>
    23                 <Route path="/signup/job-seeker" element={<SignUpJobSeekerForm/>}></Route>
    24                 <Route path="/my-job-advertisements" element={<JobAdvertisements/>}></Route>
    25                 <Route path="/my-applications" element={<ApplicationsByJobSeeker/>}></Route>
    26                 <Route path="/job-advertisements/:id" element={<JobAdDetails/>}></Route>
    27                 <Route path="/my-job-advertisements/:advertisement_id/applications" element={<ApplicationsByJobAd/>}></Route>
    28                 <Route path="/admin-panel" element={<AdminPanel/>}></Route>
    29             </Routes>
     182        <Routes>
     183            {filterRoutes(role).map(route => (
     184                <Route
     185                    key={route.path}
     186                    path={route.path}
     187                    element={<route.component/>}
     188                    exact={route.exact}
     189                />
     190            ))}
     191            <Route path="*" element={<ErrorPage to="/"/>}/>
     192        </Routes>
     193
     194        // <Routes>
     195        //     <Route path="/" element={<Dashboard/>}></Route>
     196        //     <Route path="/signin" element={<SignInForm/>}></Route>
     197        //     <Route path="/signup/recruiter" element={<SignUpRecruiterForm/>}></Route>
     198        //     <Route path="/signup/job-seeker" element={<SignUpJobSeekerForm/>}></Route>
     199        //     <Route path="/job-management-hub" element={<Workspace/>}></Route>
     200        //     <Route path="/my-applications" element={<ApplicationsByJobSeeker/>}></Route>
     201        //     <Route path="/job-advertisements/:id" element={<JobAdDetails/>}></Route>
     202        //     <Route path="/recruiters/:id" element={<RecruiterProfile/>}></Route>
     203        //     <Route path="/job-management-hub/applications/:advertisement_id" element={<ApplicationsByJobAd/>}></Route>
     204        //     <Route path="/admin-panel" element={<AdminPanel/>}></Route>
     205        //     <Route path="/about" element={<AboutUs/>}></Route>
     206        //     <Route path="/job-seeker/edit-profile" element={<JobSeekerEditProfile/>}></Route>
     207        //     <Route path="/recruiter/edit-profile" element={<RecruiterEditProfile/>}></Route>
     208        // </Routes>
    30209    )
    31210}
     211
     212export default RoutesConfig
  • jobvista-frontend/src/enumerations/Roles.js

    rb248810 rbefb988  
    1414    RECRUITER: 'ROLE_RECRUITER',
    1515    JOBSEEKER: 'ROLE_JOBSEEKER',
     16    GUEST: 'ROLE_GUEST'
    1617};
  • jobvista-frontend/src/redux/actionTypes.js

    rb248810 rbefb988  
    2020export const FETCH_RECRUITERS = "FETCH_RECRUITERS"
    2121export const CHANGE_ACCESS = "CHANGE_ACCESS"
     22export const SUBMIT_RECRUITER_LOGO = "SUBMIT_RECRUITER_LOGO"
     23export const SUBMIT_RECRUITER_COVER = "SUBMIT_RECRUITER_COVER"
     24export const SET_LOGO_URL = 'SET_LOGO_URL';
     25export const SET_PROFILE_PIC_URL = 'SET_PROFILE_PIC_URL';
    2226
    23 
  • jobvista-frontend/src/redux/actions/applicationActions.js

    rb248810 rbefb988  
    4747      }
    4848    },
    49     fetchApplicationsByJobSeeker: (callback) => {
     49    fetchApplicationsByJobSeeker: (jobSeekerId, callback) => {
    5050      return dispatch => {
    51           let currentUser = JSON.parse(localStorage.getItem(CURRENT_USER));
    52           axios.get("/my-applications/" + currentUser.id)
     51          axios.get("/my-applications/" + jobSeekerId)
    5352              .then(response => {
    5453                  dispatch({
  • jobvista-frontend/src/redux/actions/authActions.js

    rb248810 rbefb988  
    4545                //const refreshToken = response.refreshToken;
    4646                const user = {
    47                     id: response.id,
    48                     email: response.email,
    4947                    name: response.name,
    5048                    role: response.role,
    5149                    access: response.hasAccess,
     50                    id: response.id,
    5251                };
    5352                dispatch({
  • jobvista-frontend/src/redux/actions/jobAdvertisementActions.js

    rb248810 rbefb988  
    8787    },
    8888
    89     fetchJobAdvertisementsByRecruiter: (callback) => {
     89    fetchJobAdvertisementsByRecruiter: (recruiterId, callback) => {
    9090        return dispatch => {
    9191            let currentUser = JSON.parse(localStorage.getItem(CURRENT_USER));
    92             axios.get("/job-advertisements/recruiter/" + currentUser.id)
     92            axios.get("/job-advertisements/recruiter/" + recruiterId)
    9393                .then(response => {
    9494                    dispatch({
     
    103103    },
    104104
    105     filterJobAdvertisementsByRecruiter: (filter, callback) => {
     105    filterJobAdvertisementsByRecruiter: (id, filter, callback) => {
    106106
    107107        let currentUser = JSON.parse(localStorage.getItem(CURRENT_USER));
    108         axios.post("/job-advertisements/recruiter/" + currentUser.id + "/filtered", filter)
     108        axios.post("/job-advertisements/recruiter/" + id + "/filtered", filter)
    109109            .then(response => {
    110110                callback(true, response)
     
    116116
    117117    fetchRecruiterDetailsById: (id, callback) => {
    118         axios.get("/recruiter/info/" + id)
     118        axios.get("/recruiter/"+id+"/info")
    119119            .then(response => {
    120120                callback(true, response)
  • jobvista-frontend/src/redux/reducers/authReducer.js

    rb248810 rbefb988  
    33import {isExpired} from "react-jwt";
    44import {CURRENT_USER, SIGN_IN, SIGN_OUT, UPDATE_TOKEN} from "../actionTypes";
     5import {jwtDecode} from "jwt-decode";
    56
    67const initialState = {
     
    1314        case SIGN_IN:
    1415            localStorage.setItem(AUTH_TOKEN, action.payload.token);
    15             localStorage.setItem(CURRENT_USER, JSON.stringify(action.payload.user));
     16            //localStorage.setItem(CURRENT_USER, JSON.stringify(action.payload.user));
    1617            return {
    1718                ...state,
     
    2122        case UPDATE_TOKEN:
    2223            let token = action.payload;
     24            let decodedToken;
    2325            let currentUser = "";
     26            if(token !=null) {
     27                try {
     28                    decodedToken = jwtDecode(token);
     29                } catch (error) {
     30                    console.log("Failed to decode token: " + error)
     31                }
     32            }
     33
    2434            if(!isExpired(token)) {
    2535                localStorage.setItem(AUTH_TOKEN, token);
    26                 currentUser = JSON.parse(localStorage.getItem(CURRENT_USER));
     36                currentUser = {
     37                    name: decodedToken.name,
     38                    role: decodedToken.role,
     39                    access: decodedToken.access,
     40                    id: decodedToken.id
     41                };
    2742            } else {
    28                 localStorage.removeItem(CURRENT_USER);
     43                //localStorage.removeItem(CURRENT_USER);
    2944                localStorage.removeItem(AUTH_TOKEN);
    3045                currentUser = "";
     
    3752            };
    3853        case SIGN_OUT:
    39             localStorage.removeItem(CURRENT_USER);
     54            //localStorage.removeItem(CURRENT_USER);
    4055            localStorage.removeItem(AUTH_TOKEN);
    4156            return {
  • jobvista-frontend/src/redux/reducers/jobAdvertisementReducer.js

    rb248810 rbefb988  
    11import {
    22    ADD_JOB_ADVERTISEMENT,
    3     CURRENT_USER, DELETE_JOB_ADVERTISEMENT, EDIT_JOB_ADVERTISEMENT,
     3    CURRENT_USER,
     4    DELETE_JOB_ADVERTISEMENT,
     5    EDIT_JOB_ADVERTISEMENT,
    46    FETCH_JOB_ADVERTISEMENTS,
    5     FETCH_JOB_ADVERTISEMENTS_BY_RECRUITER, FILTER_JOB_ADVERTISEMENTS, FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER
     7    FETCH_JOB_ADVERTISEMENTS_BY_RECRUITER,
     8    FILTER_JOB_ADVERTISEMENTS,
     9    FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER,
     10    SET_LOGO_URL
    611} from "../actionTypes";
    712import {sortElementsBy} from "../../utils/utils";
  • jobvista-frontend/src/redux/store.js

    rb248810 rbefb988  
    66import adminReducer from "./reducers/adminReducer"
    77import {AdminActions} from "./actions/adminActions";
     8import ImagesReducer from "./reducers/imagesReducer";
    89
    910// const rootReducer = combineReducers({
     
    2122        jobAd: jobAdReducer,
    2223        appl: applicationReducer,
    23         admin: adminReducer
     24        admin: adminReducer,
     25        images: ImagesReducer
    2426    },
    2527});
  • jobvista-frontend/src/views/admin_panel/AdminPanel.js

    rb248810 rbefb988  
    44import {useState, useEffect} from "react";
    55import {AdminActions} from "../../redux/actions/adminActions";
     6import {notifyAccessUpdate} from "../../utils/toastUtils";
    67
    78export const AdminPanel = () => {
     
    2425            setRecruiters(recruitersState)
    2526            console.log("Fetch all recruiters STATE")
     27            console.log(recruitersState)
     28
    2629        }
    2730    }, [recruitersState])
     
    3235    };
    3336
    34     const handleAccessChange = (recruiterId, newAccessStatus) => {
     37    const handleAccessChange = (recruiterId, companyName, newAccessStatus) => {
     38
    3539        setRecruiters(prevState =>
    3640            prevState.map(recruiter =>
     
    4145        );
    4246
    43         console.log(recruiterId + " " + newAccessStatus)
    44 
    4547        dispatch(AdminActions.changeAccess(recruiterId, newAccessStatus, (success, response) => {
    4648            if(success) {
    47                 console.log("Access changed")
     49                notifyAccessUpdate(companyName)
    4850            }
    4951        }));
     
    5153
    5254    return (
    53         <div className="applications-container mt-5">
     55        <div className="custom-container mt-5">
    5456            <table className="table table-striped">
    5557                <thead>
     
    6466                </thead>
    6567                <tbody>
    66                 {recruiters.map((recruiter) => (
     68                {recruitersState.map((recruiter) => (
    6769                    <tr key={recruiter.id}>
    6870                        <th scope="row">{recruiter.id}</th>
     
    7072                        <td>{recruiter.email}</td>
    7173                        <td>{recruiter.companyName}</td>
    72                         <td>{recruiter.phoneNumber}</td>
     74                        <td>{recruiter.contactPhoneNumber}</td>
    7375                        <td>
    7476                            <label className="switch">
     
    7678                                    type="checkbox"
    7779                                    checked={recruiter.hasAccess}
    78                                     onChange={(e) => handleAccessChange(recruiter.id, e.target.checked)}
     80                                    onChange={(e) => handleAccessChange(recruiter.id, recruiter.companyName, e.target.checked)}
    7981                                />
    8082                                <span className="slider"></span>
  • 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>
  • jobvista-frontend/src/views/auth/SignInForm.js

    rb248810 rbefb988  
    88import {yupResolver} from "@hookform/resolvers/yup";
    99import {AuthActions} from "../../redux/actions/authActions";
     10import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils";
    1011
    1112export const SignInForm = () => {
     
    2728    const signIn = async (values) => {
    2829        try {
    29             dispatch(AuthActions.signIn(values.emailLog, values.passwordLog,
    30                     success => {
    31                     // createSnackbar({
    32                     //     message: success ? 'Successfully signed up.' : 'Error while signing up. Try again!',
    33                     //     timeout: 2500,
    34                     //     theme: success ? 'success' : 'error'
    35                     // });
    36                     success && navigate("/");
     30            dispatch(AuthActions.signIn(values.emailLog, values.passwordLog, success => {
     31                    if(success) {
     32                        navigate("/")
     33                    } else {
     34                        notifyIncorrectEmailOrPassword()
     35                    }
    3736                }));
    3837        } catch (err) {
    39             console.error(err);
     38            // console.error(err);
    4039        }
    4140    }
  • jobvista-frontend/src/views/dashboard/Dashboard.js

    rb248810 rbefb988  
    1 import "./Dashboard.css"
     1import "../shared_css/Random.css"
    22
    33import {useDispatch, useSelector} from "react-redux";
     
    1212import {AUTH_TOKEN} from "../../axios/axiosInstance";
    1313import {jwtDecode} from "jwt-decode";
    14 
    15 export const Dashboard = (props) => {
     14import {RecruiterActions} from "../../redux/actions/recruiterActions";
     15
     16export const Dashboard = () => {
    1617
    1718    const dispatch = useDispatch();
     
    1920    const [jobAdvertisements, setJobAdvertisements] = useState([]);
    2021    let jobAdvertisementsState = useSelector(state => state.jobAd.jobAdvertisements)
     22    const [jobDispatched, setJobDispatched] = useState(false);
     23
     24    const [logos, setLogos] = useState({});
     25    let logosState = useSelector(state => state.images.logos)
     26    const [logoDispatched, setLogoDispatched] = useState(false);
     27
    2128    const auth = useSelector(state => state.auth);
    2229
     
    2532    const [selectedIndustry, setSelectedIndustry] = useState("all");
    2633    const [searchTerm, setSearchTerm] = useState("");
    27     const [dispatched, setDispatched] = useState(false)
     34
     35
    2836
    2937    // const [user, setUser] = useState(null);
     
    5462
    5563    useEffect(() => {
    56         if (!dispatched && jobAdvertisementsState.length == 0) {
     64        if (!jobDispatched && jobAdvertisementsState.length == 0) {
    5765            dispatch(JobAdvertisementActions.fetchJobAdvertisements((success, response) => {
    5866                if (success && response.data.length > 0) {
    59                     setJobAdvertisements(sortElementsBy(response.data))
     67                    setJobAdvertisements(sortElementsBy(response.data, "postedOn"))
    6068                }
    61                 setDispatched(true)
     69                setJobDispatched(true)
    6270                console.log("Fetch all job advertisements GET")
    6371            }))
     
    6674            setJobAdvertisements(jobAdvertisementsState)
    6775            console.log("Fetch all job advertisements STATE")
     76
     77
    6878        }
    6979    }, [jobAdvertisementsState])
     80
     81    useEffect(() => {
     82
     83        if(jobDispatched && !logoDispatched) {
     84            jobAdvertisements.forEach(jobAd => {
     85                if(jobAd.recruiterId && !logos[jobAd.recruiterId]) {
     86                    fetchLogo(jobAd.recruiterId);
     87                }
     88            })
     89            setLogoDispatched(true)
     90            console.log("Fetch all logos GET")
     91        } else {
     92            setLogos(logosState)
     93            console.log("Fetch all logos STATE")
     94        }
     95
     96        }, [jobDispatched, logosState])
     97
     98    const fetchLogo = (recruiterId) => {
     99        dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => {
     100            if(success) {
     101                setLogos(prevLogos => ({...prevLogos, [recruiterId]: response}))
     102            }
     103        }));
     104    };
    70105
    71106    let filterJobAdvertisements = () => {
     
    86121
    87122        <div className="container">
    88             <div className="head-dashboard-box">
     123            <div className="filter-container">
    89124                <div className="row">
    90                     <div className="col-md-12 filter-container">
     125                    <div className="col-md-12 filter-box">
    91126                        <div className="search-container">
    92127                            <i className="fa-solid fa-magnifying-glass search-icon"></i>
     
    117152                            />
    118153                        </div>
    119                         <button onClick={filterJobAdvertisements} className="btn-open-modal">Find jobs</button>
     154                        <button onClick={filterJobAdvertisements} className="blue-submit-button">Find jobs</button>
    120155                    </div>
    121                     {/*<div className="col-md-3">*/}
    122 
    123                     {/*<div className="date-range-section item">*/}
    124                     {/*    <Select*/}
    125                     {/*        defaultValue={{value: "all", label: "Lifetime"}}*/}
    126                     {/*        //value={selectedDateRange.value}*/}
    127                     {/*        //onChange={option => setSelectedDateRange(option.value)}*/}
    128                     {/*        options={dataRangeOptions}*/}
    129                     {/*        className="date-range sort"*/}
    130                     {/*    />*/}
    131                     {/*</div>*/}
    132                     {/*</div>*/}
    133156                </div>
    134157            </div>
    135             <div className="row row-cols-1 row-cols-md-4 g-4">
    136 
     158            <div className="row row-cols-2 row-cols-md-4 g-4">
    137159                {jobAdvertisements &&
    138160                    jobAdvertisements.map((jobAd, index) => (
     
    146168                                </div>
    147169                                <div className="card-body">
     170                                    <img
     171                                        // loading gif
     172                                        src={logos[jobAd.recruiterId]}
     173                                        alt=""
     174                                        width={100} height={100}
     175                                    />
    148176                                    <h5 className="card-title">{jobAd.title}</h5>
    149177                                    <span>{jobAd.industry} • <span style={{
  • jobvista-frontend/src/views/job_advertisements/AddJobAdModal.js

    rb248810 rbefb988  
    11import React, {useState} from "react";
    2 import "./Form.css";
     2import "../shared_css/Modal.css";
    33
    44import { Editor } from 'primereact/editor';
     
    1717import {useDispatch, useSelector} from "react-redux";
    1818import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
     19import {notifyJobAdPost} from "../../utils/toastUtils";
    1920
    2021
     
    3233        description: yup.string().required("Please enter a description"),
    3334        industry: yup.mixed().required("Select industry"),
    34         startingSalary: yup.number().required("Please enter the starting salary"),
     35        startingSalary: yup.string().required("Please enter the starting salary"),
    3536        jobType: yup.mixed().required("Select job type"),
    3637        employmentStatus: yup.mixed().required("Select employment status"),
     
    4647            dispatch(JobAdvertisementActions.addJobAdvertisement(
    4748                {
    48                     email: auth.email,
     49                    id: auth.id,
    4950                    title: values.title,
    5051                    description: values.description,
     
    5657                }, (success, response) => {
    5758                    if (success) {
    58                         console.log("Job Advertisement added")
     59                        // console.log("Job Advertisement added")
    5960                        toggleModal()
     61                        notifyJobAdPost()
    6062                    }
    6163                }
     
    7577        </div>
    7678        {/*<button onClick={toggleModal} className="btn-open-modal">POST ADVERTISEMENT</button>*/}
    77         <Modal open={modal} onClose={toggleModal} center classNames="job-advertisement-modal">
     79        <Modal open={modal} onClose={toggleModal} center>
    7880            <div className="head-modal">
    7981                <h3>Post Job Advertisement</h3>
     
    8284
    8385            <div className="modal-content">
    84                 <form onSubmit={handleSubmit(addJobAdvertisement)}>
     86                <form>
    8587                    <div className="row">
    8688                        <div className="col-md-7">
     
    111113                        <div className="col-md-5">
    112114                            <label className="label">Hourly rate:</label>
    113                             <input {...register("startingSalary")}/>
     115                            <input type="number" {...register("startingSalary")}/>
    114116                            <p style={{color: "red"}}>{errors.startingSalary?.message}</p>
    115117
     
    157159                    </div>
    158160
    159                     <div className="aligned">
    160                         <button className="submit-btn"> Submit</button>
     161                    <div className="modal-buttons">
     162                        <div className="cancel-btn" onClick={toggleModal}> Cancel</div>
     163                        <button className="submit-btn" onClick={handleSubmit(addJobAdvertisement)}>Submit</button>
    161164                    </div>
    162165
  • jobvista-frontend/src/views/job_advertisements/DeleteJobAdModal.js

    rb248810 rbefb988  
    11import React, {useState} from "react";
    2 import "./Form.css";
     2import "../shared_css/Modal.css";
    33
    44import 'react-responsive-modal/styles.css';
     
    1515import {useDispatch, useSelector} from "react-redux";
    1616import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
     17import {notifyJobAdDelete} from "../../utils/toastUtils";
    1718
    1819
     
    2930            dispatch(JobAdvertisementActions.deleteJobAdvertisement(jobAd.props.id, (success, response) => {
    3031                if (success) {
    31                     console.log("Job Advertisement deleted")
     32                    // console.log("Job Advertisement deleted")
    3233                    toggleModal()
     34                    notifyJobAdDelete()
    3335                }
    3436            }))
  • jobvista-frontend/src/views/job_advertisements/EditJobAdModal.js

    rb248810 rbefb988  
    11import React, {useEffect, useState} from "react";
    2 import "./Form.css";
     2import "../shared_css/Modal.css";
    33
    44import 'react-responsive-modal/styles.css';
     
    1616import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";
    1717import {Editor} from "primereact/editor";
     18import {notifyJobAdDelete, notifyJobAdEdit} from "../../utils/toastUtils";
    1819
    1920
     
    2324    const auth = useSelector(state => state.auth.currentUser)
    2425    const toggleModal = () => {
     26        console.log("NAD SET")
    2527        setModal(!modal);
     28        console.log("POD SET")
     29
    2630    };
    2731
     
    5256                }, jobAd.props.id, (success, response) => {
    5357                    if(success) {
    54                         console.log("Job Advertisement edited")
     58                        // console.log("Job Advertisement edited")
    5559                        toggleModal()
     60                        notifyJobAdEdit()
    5661                    }
    5762                }
     
    7883
    7984            <div className="modal-content">
    80                 <form onSubmit={handleSubmit(editJobAdvertisement)}>
     85                <form>
    8186                    <div className="row">
    8287                        <div className="col-md-7">
     
    9297                                name="description"
    9398                                control={control}
     99                                defaultValue={jobAd.props.description}
    94100                                render={({ field }) => (
    95101                                    <Editor
    96                                         defaultValue={jobAd.props.description}
     102                                        // defaultValue={jobAd.props.description}
    97103                                        value={jobAd.props.description}
    98104                                        onTextChange={(e) => field.onChange(e.htmlValue)}
     
    107113                        <div className="col-md-5">
    108114                            <label className="label">Hourly rate:</label>
    109                             <input defaultValue={jobAd.props.startingSalary} {...register("startingSalary")}/>
     115                            <input type="number" defaultValue={jobAd.props.startingSalary} {...register("startingSalary")}/>
    110116                            <p style={{color: "red"}}>{errors.startingSalary?.message}</p>
    111117
     
    166172                    </div>
    167173
    168                     <div className="aligned">
    169                         <button className="submit-btn"> Submit</button>
     174                    <div className="modal-buttons">
     175                        <div className="cancel-btn" onClick={toggleModal}> Cancel</div>
     176                        <button className="submit-btn" onClick={handleSubmit(editJobAdvertisement)}> Save changes</button>
    170177                    </div>
    171 
    172178                </form>
    173179            </div>
  • jobvista-frontend/src/views/job_advertisements/JobAdDetails.css

    rb248810 rbefb988  
    1010.details-wrap {
    1111    width: 100%;
    12     //height: auto;
    13     height: 80vh;
     12    height: auto;
     13    max-height: 80vh;
    1414    overflow-y: auto;
     15    background-color: #fff;
     16    border-radius: 12px;
     17    padding: 15px 20px;
     18    margin-bottom: 20px;
     19
     20    /*scrollbar-width: thin; !* "auto" hides scrollbar on some browsers, "thin" shows a thin scrollbar *!*/
     21    /*scrollbar-color: #999999 #fff;*/
     22}
     23
     24.details-wrap .span-about {
     25    color: darkgray;
     26}
     27
     28
     29
     30.details-wrap-profile {
     31    width: 100%;
     32    height: auto;
     33
    1534    background-color: #fff;
    1635    border-radius: 12px;
     
    5675}
    5776
     77.apply-button {
     78    border: 0;
     79    border-radius: 8px;
     80    width: 45%;
     81    background-image: linear-gradient(to right, #a1c4fd 0%, aliceblue 61%, #a1c4fd 100%);
     82    background-size: 200% auto;
     83    font-weight: bold;
     84    padding: 5px 10px;
     85    transition: 0.4s;
     86}
     87
     88.apply-button:not(.disabled):hover{
     89    background-color: rgb(187, 215, 235);
     90    color: black;
     91}
     92
     93.disabled {
     94    cursor: default !important;
     95    opacity: 0.6;
     96}
     97
    5898.apply:not(.expired) {
    5999    width: 20% !important;
     
    65105    background-position: right center;
    66106}
     107
     108.recruiter-link {
     109    color: black;
     110    font-size: 30px;
     111    text-decoration:none;
     112}
  • jobvista-frontend/src/views/job_advertisements/JobAdDetails.js

    rb248810 rbefb988  
    1010import {AddJobAdModal} from "./AddJobAdModal";
    1111import {ApplyToJobAdModal} from "../applications/ApplyToJobAdModal";
     12import {Link} from "react-router-dom";
     13import {RecruiterActions} from "../../redux/actions/recruiterActions";
    1214
    1315
     
    2022    const auth = useSelector(state => state.auth.currentUser);
    2123
     24    let logosState = useSelector(state => state.images.logos)
     25    const [logoDispatched, setLogoDispatched] = useState(false)
     26    const [logoView, setLogoView] = useState(null);
     27
    2228    useEffect(() => {
    2329        setRole(auth.role)
    2430    }, [auth])
     31
     32    useEffect(() => {
     33        if (jobAd) {
     34            if (!logoDispatched && !logosState[jobAd.recruiterId]) {
     35                dispatch(RecruiterActions.downloadLogo(jobAd.recruiterId, (success, response) => {
     36                    if (success) {
     37                        setLogoView(response)
     38                        setLogoDispatched(true)
     39                    }
     40                }))
     41            } else {
     42                setLogoView(logosState[jobAd.recruiterId])
     43            }
     44
     45        }
     46
     47    }, [jobAd])
    2548
    2649
     
    4164            <div className="col-md-9">
    4265                <div className="details-wrap">
     66                    <div className="row">
     67                        <div className="col-md-9">
     68                            <div className="title">
     69                                <h2>{jobAd.title} </h2>
     70                                <span className="job-type"> {jobAd.jobType===JobType.JOB ? "Job" : "Internship"}</span>
     71                                {!jobAd.active && <span className="expired">Expired</span>}
     72                            </div>
    4373
    44                     <div className="title">
    45                         <h2>{jobAd.title} </h2>
    46                         <span className="job-type"> {jobAd.jobType===JobType.JOB ? "Job" : "Internship"}</span>
    47                         {!jobAd.active && <span className="expired">Expired</span>}
     74                            <p className="details-head-info">
     75                                <span><b>{jobAd.recruiterName}</b></span> • <span>{jobAd.industry}</span> • <span>{formatRelativeTime(jobAd.postedOn)}</span>
     76                            </p>
     77
     78                            <p><i className="fa-solid fa-money-check-dollar"></i> <span>Hourly rate: ${jobAd.startingSalary}</span></p>
     79                            <p><i className="fa-solid fa-briefcase"></i> Employment status: {jobAd.employmentStatus==="FULL_TIME" ? "Full-time" : "Part-time"}</p>
     80                            <p><i className="fa-solid fa-calendar-days"></i> Active until: {new Date(jobAd.activeUntil).toLocaleString('default', { day: 'numeric', month: 'long',  year: 'numeric' })}</p>
     81
     82                        </div>
     83                        <div className="col-md-3">
     84                            {jobAd.recruiterId &&
     85                                <>
     86                                    <img
     87                                        // loading gif
     88                                        src={logosState[jobAd.recruiterId]}
     89                                        alt=""
     90                                        width={200} height={200}
     91                                    />
     92                                </>
     93                            }
     94
     95                        </div>
    4896                    </div>
    4997
    50                     <p className="details-head-info">
    51                         <span><b>{jobAd.recruiterName}</b></span> • <span>{jobAd.industry}</span> • <span>{formatRelativeTime(jobAd.postedOn)}</span>
    52                     </p>
    53 
    54                     <p><i className="fa-solid fa-money-check-dollar"></i> <span>Hourly rate: ${jobAd.startingSalary}</span></p>
    55                     <p><i className="fa-solid fa-briefcase"></i> Employment status: {jobAd.employmentStatus==="FULL_TIME" ? "Full-time" : "Part-time"}</p>
    56                     <p><i className="fa-solid fa-calendar-days"></i> Active until: {new Date(jobAd.activeUntil).toLocaleString('default', { day: 'numeric', month: 'long',  year: 'numeric' })}</p>
    5798
    5899                    <h4>About the job</h4>
     
    66107            <div className="col-md-3">
    67108                <div className="details-wrap">
    68                     <h3>{jobAd.recruiterName}</h3>
     109                    <Link className="recruiter-link" to={`/recruiters/${jobAd.recruiterId}`}>{jobAd.recruiterName} </Link>
    69110
    70                     {/*TO DO - AFTER IMPLEMENTING FORM FOR UPDATING PERSONAL INFO*/}
    71111                    <h4>About the company</h4>
    72112                    <p>
    73                         For over two decades, we have been harnessing technology to drive meaningful change.  Working side by side with leading brands, we build strategies, products and solutions tailored to unique needs –regardless of industry, region or scale. By combining world-class engineering, industry expertise and a people-centric mindset, we consult and partner with our customers to create technological solutions that drive innovation and transform businesses.
    74                         <br/><br/>
    75                         From ideation to production, we support our customers with bespoke solutions across various industries, including payments, insurance, finance and banking, technology, media and entertainment, telecommunications, retail and consumer goods, supply chain and logistics, healthcare and life sciences, energy and resources, government, automotive and travel.
     113                        {recruiterDetails.companyDescription
     114                            ? recruiterDetails.companyDescription.length > 710
     115                                ? `${recruiterDetails.companyDescription.substring(0, 710)}...`
     116                                : recruiterDetails.companyDescription
     117                            : "There is no info about this company yet."
     118                        }
     119                    </p>
    76120
     121                    <p>
     122                        <i className="fa-solid fa-envelope"></i> <span className="span-about"> {recruiterDetails.contactEmail}</span> <br/>
     123                        <i className="fa-solid fa-phone"></i> <span className="span-about"> {recruiterDetails.contactPhoneNumber}</span> <br/>
     124                        {recruiterDetails.receptionist && <><i className="fa-solid fa-user-tie"></i> <span className="span-about"> {recruiterDetails.receptionist}</span> </>}
    77125                    </p>
    78                     <p><span><i className="fa-solid fa-envelope"></i> {recruiterDetails.email}</span> • <span>
    79                 <i className="fa-solid fa-phone"></i> {recruiterDetails.phoneNumber}</span>
    80                     </p>
     126
     127                    <div className="d-flex justify-content-center mt-4">
     128                        <Link className="card-button" to={`/recruiters/${jobAd.recruiterId}`}>Read more </Link>
     129                    </div>
    81130                </div>
    82131            </div>
  • jobvista-frontend/src/views/job_advertisements/JobAdvertisements.css

    rb248810 rbefb988  
     1.photo-box {
     2    position: relative;
     3    margin-bottom: 140px;
     4}
     5
     6.photo-box div img {
     7    background-color: rgb(243, 242, 241);
     8}
     9
     10.company-banner {
     11    width: 100%;
     12    height: 300px;
     13    object-fit: cover;
     14    object-position: initial;
     15    display: block;
     16    border-radius: 10px;
     17}
     18
     19.company-logo {
     20    position: absolute;
     21    border-radius: 25px;
     22    border: 5px solid rgb(243, 242, 241);
     23    bottom: -110px;
     24    left: 70px
     25}
     26
     27.info-tab {
     28    margin: 20px 10px;
     29    position: absolute;
     30    left: 280px;
     31    font-family: Poppins;
     32}
     33
     34.photo-box .edit-buttons {
     35    display: flex;
     36    gap: 10px;
     37    position: absolute;
     38    right: 20px;
     39    bottom: -100px;
     40}
     41
     42.edit-buttons .exclamation {
     43    display: inline;
     44    position: relative;
     45}
     46
     47.fa-circle-exclamation {
     48    color: #d80e0e;
     49
     50}
     51
     52.edit-buttons .modal-wrap .react-responsive-modal-modal .head-modal {
     53    background-color: white !important;
     54    color: black !important;
     55}
     56
     57.edit-buttons button {
     58    background-color: white;
     59    border: none;
     60    border-radius: 5px;
     61    padding: 7px 13px;
     62    transition: 0.1s;
     63}
     64
     65.edit-buttons button:hover {
     66    /*background-color: #F2F2F2;*/
     67    background-color: lightgray;
     68}
     69
     70.my-workspace {
     71    position: relative;
     72}
     73
     74.custom-text-area {
     75    height: 14rem !important;
     76}
     77
     78.line-separator {
     79    width: 95%;
     80    border-top: 1px solid gray;
     81    border-radius: 5px;
     82    margin: auto;
     83    margin-bottom: 30px;
     84}
     85
     86
     87.confirmation-bar {
     88    position: fixed;
     89    //top: 80px;
     90    left: 0;
     91    width: calc(100% - 17px);
     92    background-color: rgba(0, 0, 0, 0.5); /* Adjust background color as needed */
     93    //background-color: #7D7D7A; /* Adjust background color as needed */
     94    padding: 10px;
     95    z-index: 11;
     96
     97}
     98.confirmation-bar-buttons {
     99    display: flex;
     100    justify-content: right;
     101    gap: 10px;
     102    margin-right: 60px
     103}
     104.confirmation-bar .confirmation-bar-buttons button {
     105    padding: 5px 20px;
     106    border: none;
     107    border-radius: 8px;
     108    color: white;
     109    transition: 0.1s;
     110}
     111.confirmation-bar .confirmation-bar-buttons .cancel-changes {
     112    background-color: #8D8D8B;
     113}
     114
     115.confirmation-bar .confirmation-bar-buttons .cancel-changes:hover {
     116    background-color: #9B9A99;
     117}
     118
     119.confirmation-bar .confirmation-bar-buttons .save-changes {
     120    background-color: #0866FF;
     121
     122}
     123.confirmation-bar .confirmation-bar-buttons .save-changes:hover {
     124    background-color: dodgerblue;
     125}
     126
  • jobvista-frontend/src/views/job_advertisements/RecruiterWorkspace.js

    rb248810 rbefb988  
    22
    33import "./JobAdvertisements.css"
     4import "../shared_css/Random.css"
     5
    46import {useDispatch, useSelector} from "react-redux";
    57import {useEffect, useState} from "react";
     
    1214import {Link} from "react-router-dom";
    1315import JobType from "../../enumerations/JobType";
     16import {RecruiterActions} from "../../redux/actions/recruiterActions";
    1417
    15 export const JobAdvertisements = (props) => {
     18
     19export const Workspace = (props) => {
    1620
    1721    const dispatch = useDispatch();
     22    const [dispatched, setDispatched] = useState(false)
     23
     24    const auth = useSelector(state => (state.auth.currentUser))
     25
    1826    const [jobAdvertisementsByRecruiter, setJobAdvertisementsByRecruiter] = useState([]);
    19     const auth = useSelector(state => (state.auth.currentUser))
    2027    let jobAdvertisementsByRecruiterState = useSelector(state => (state.jobAd.jobAdvertisementsByRecruiter))
    2128
    22     const [role, setRole] = useState("");
     29    const [recruiterDetails, setRecruiterDetails] = useState(null);
     30
    2331    const [selectedSortOrder, setSelectedSortOrder] = useState("date_newest");
    2432    const [selectedIndustry, setSelectedIndustry] = useState("all");
    2533    const [searchTerm, setSearchTerm] = useState("");
    26     const [dispatched, setDispatched] = useState(false)
     34
     35    const [activeJobListingsCount, setActiveJobListingsCount] = useState(0);
     36
     37    useEffect(() => {
     38        if (auth) {
     39            dispatch(RecruiterActions.fetchRecruiterEditDetailsById(auth.id, (success, response) => {
     40                if (success) {
     41                    setRecruiterDetails(response.data)
     42                }
     43            }))
     44        }
     45    }, [auth]);
    2746
    2847
    2948    useEffect(() => {
    30         if (auth) {
    31             setRole(auth.role);
    32         }
    33     }, [auth]);
    34 
    35     useEffect(() => {
    3649        if (!dispatched && jobAdvertisementsByRecruiterState.length === 0) {
    37             dispatch(JobAdvertisementActions.fetchJobAdvertisementsByRecruiter((success, response) => {
     50            dispatch(JobAdvertisementActions.fetchJobAdvertisementsByRecruiter(auth.id, (success, response) => {
    3851                if (success && response.data.length > 0) {
    3952                    setJobAdvertisementsByRecruiter(sortElementsBy(response.data))
     
    4659            setJobAdvertisementsByRecruiter(jobAdvertisementsByRecruiterState)
    4760            console.log("Fetch job advertisements by recruiter STATE")
     61
     62            setActiveJobListingsCount(countActiveJobListings(jobAdvertisementsByRecruiterState));
    4863        }
     64
    4965    }, [jobAdvertisementsByRecruiterState])
    5066
    5167    let filterJobAdvertisements = () => {
    52         JobAdvertisementActions.filterJobAdvertisementsByRecruiter(
    53             {
    54                 searchTerm: searchTerm,
    55                 industry: selectedIndustry,
    56                 sortOrder: selectedSortOrder
    57             }, (success, response) => {
    58                 if (success) {
    59                     setJobAdvertisementsByRecruiter(response.data);
    60                 }
     68        JobAdvertisementActions.filterJobAdvertisementsByRecruiter({
     69            searchTerm: searchTerm, industry: selectedIndustry, sortOrder: selectedSortOrder
     70        }, (success, response) => {
     71            if (success) {
     72                setJobAdvertisementsByRecruiter(response.data);
    6173            }
    62         )
     74        })
    6375    }
    6476
    65     return (
    66         <div className="container">
    67             <div className="head-dashboard-box">
     77    function countActiveJobListings(jobAds) {
     78        if (jobAds.length > 0) {
     79            const activeJobListings = jobAds.filter(job => job.active)
     80            return activeJobListings.length;
     81        }
     82        return 0;
     83    }
     84
     85    return (<div className="container">
     86
     87            {/*<div className="line-separator"></div>*/}
     88
     89            <div className="filter-container">
    6890                <div className="row">
    69                     <div className="col-md-12 filter-container">
     91                    <div className="col-md-12 filter-box">
    7092                        <div className="search-container">
    7193                            <i className="fa-solid fa-magnifying-glass search-icon"></i>
     
    96118                            />
    97119                        </div>
    98                         <button onClick={filterJobAdvertisements} className="btn-open-modal">Find jobs</button>
     120                        <button onClick={filterJobAdvertisements} className="blue-submit-button">Find jobs</button>
    99121                    </div>
    100122                </div>
    101123            </div>
    102             <div className="row row-cols-1 row-cols-md-4 g-4">
     124            <div className="row row-cols-1 row-cols-md-5 g-3">
    103125                <AddJobAdModal/>
    104126
    105                 {jobAdvertisementsByRecruiter &&
    106                     jobAdvertisementsByRecruiter.map((jobAd, index) => (
    107                         <div key={index} className="col">
    108                             <div className="custom-card">
    109                                 <div className="card-head">
    110                                     <span className="hourly-salary"><b>${jobAd.startingSalary}/hr</b></span>
    111                                     <span
    112                                         className="job-type"> {jobAd.jobType === JobType.JOB ? "Job" : "Internship"}</span>
    113                                     {!jobAd.active && <span className="expired">Expired</span>}
    114                                     <div className="card-management-btns">
    115                                         <DeleteJobAdModal props={jobAd}/>
    116                                         <EditJobAdModal props={jobAd}/>
    117                                     </div>
     127                {jobAdvertisementsByRecruiter && jobAdvertisementsByRecruiter.map((jobAd, index) => (
     128                    <div key={index} className="col">
     129                        <div className="custom-card">
     130                            <div className="card-head">
     131                                <span className="hourly-salary"><b>${jobAd.startingSalary}/hr</b></span>
     132                                <span
     133                                    className="job-type"> {jobAd.jobType === JobType.JOB ? "Job" : "Internship"}</span>
     134                                {!jobAd.active && <span className="expired">Expired</span>}
     135                                <div className="card-management-btns">
     136                                    <DeleteJobAdModal props={jobAd}/>
     137                                    <EditJobAdModal props={jobAd}/>
    118138                                </div>
    119                                 <div className="card-body">
    120                                     <h5 className="card-title">{jobAd.title}</h5>
    121                                     <span>{jobAd.industry} • <span style={{
    122                                         color: "black",
    123                                         fontWeight: "bold"
    124                                     }}>{formatRelativeTime(jobAd.postedOn)}</span></span>
    125                                     <div className="card-info">
     139                            </div>
     140                            <div className="card-body">
     141                                <h5 className="card-title">{jobAd.title}</h5>
     142                                <span>{jobAd.industry} • <span style={{
     143                                    color: "black", fontWeight: "bold"
     144                                }}>{formatRelativeTime(jobAd.postedOn)}</span></span>
     145                                <div className="card-info">
    126146                                        <span><i className="fa-solid fa-building"
    127147                                                 style={{color: "#000000"}}></i> Company: <span style={{
    128                                             color: "black",
    129                                             fontWeight: "bold"
     148                                            color: "black", fontWeight: "bold"
    130149                                        }}>{jobAd.recruiterName}</span></span> <br/>
    131                                     </div>
     150                                </div>
    132151
    133                                     <div className="aligned">
    134                                         <Link to={`/my-job-advertisements/${jobAd.id}/applications`}
    135                                               className="card-button solo">View applications</Link>
    136                                     </div>
     152                                <div className="aligned">
     153                                    <Link to={`/job-management-hub/applications/${jobAd.id}`}
     154                                          className="card-button solo">View applications</Link>
     155                                </div>
    137156
    138                                 </div>
    139157                            </div>
    140158                        </div>
    141                     ))}
    142 
     159                    </div>))}
    143160            </div>
    144161        </div>
     162
     163
    145164    )
    146165}
  • jobvista-frontend/src/views/shared_css/Modal.css

    rb248810 rbefb988  
    22.modal-wrap {
    33    display: inline;
    4 }
    5 .btn-open-modal {
    6     font-size: 18px;
    7     border-radius: 10px;
    8     padding: 5px 10px;
    9     background-color: #06367a;
    10     border: none;
    11     color: #fff;
    12     font-weight: 600;
    13     float: right;
    14     /*margin-left: 10px;*/
    154}
    165
     
    2615    background: #f1f1f1;
    2716    border-radius: 10px;
    28     width: 1300px !important;
    29     //min-width: 300px;
     17    min-width: 1000px !important;
     18    max-width: 1000px !important;
    3019    height: auto;
    3120    padding: 0 !important;
     
    4130    border-top-right-radius: 10px;
    4231    background-color: rgba(1,38,90,0.9);
     32    /*background-color: white;*/
    4333    background-size: cover;
    4434    padding: 14px 48px;
     35    /*color: #3A3B3C;*/
    4536    color: white;
     37    border-bottom: 1px solid #E5E4E7;
     38    display: flex;
     39    justify-content: left;
    4640}
    4741
     
    5044    font-weight: bold;
    5145    display: inline;
     46    margin: 0;
    5247}
    5348
     
    6055}
    6156
    62 .react-responsive-modal-modal .modal-content form .aligned {
    63     text-align: center;
     57.react-responsive-modal-modal .modal-content form .modal-buttons{
     58    display: flex;
     59    justify-content: end;
     60    gap: 10px;
     61    margin-right: 10px;
    6462}
    6563
     
    6967}
    7068
    71 input {
     69.react-responsive-modal-modal .modal-content form input {
    7270    display: block;
    7371    padding: 5px 10px;
     
    7775}
    7876
    79 textarea {
     77.react-responsive-modal-modal .modal-content form textarea {
    8078    width: 100%;
    8179    resize: none;
     
    8987}
    9088
    91 .applictaion-textarea {
     89.application-textarea {
    9290    height: 100px;
    9391}
     
    108106}
    109107
    110 .submit-btn {
     108.modal-buttons button, .modal-buttons div {
    111109    position: relative;
    112110    padding: 5px 20px;
     
    114112    border: none;
    115113    color: white;
    116     text-transform: uppercase;
    117     font-weight: bold;
    118     width: 30%;
    119     /*clip-path: polygon(100% 0, 100% 75%, 95% 100%, 0 100%, 0 0);*/
    120     background: linear-gradient(to right, #06367a, #06367a);
     114    /*text-transform: uppercase;*/
     115    font-weight: 500;
    121116    margin-top: 15px;
     117    cursor: pointer
     118}
     119
     120.modal-buttons .submit-btn {
     121    background: #06367a;
     122    transition: background-color 0.3s ease, color 0.3s ease;
     123}
     124
     125.modal-buttons .submit-btn:hover {
     126    background-color: #2e579b;
     127}
     128
     129.modal-buttons .cancel-btn {
     130    color: #858B96;
     131    background-color: #F0F0F0;
     132    transition: background-color 0.3s ease, color 0.3s ease;
     133}
     134.modal-buttons .cancel-btn:hover {
     135    background-color: #e0e0e0;
     136    color: #555;
    122137}
    123138
     
    156171
    157172
    158 .cancel-btn, .delete-btn {
     173.modal-delete-buttons .cancel-btn, .modal-delete-buttons .delete-btn {
    159174    display: inline;
    160175    border: none;
     
    163178}
    164179
    165 .delete-btn {
     180.modal-delete-buttons .delete-btn {
    166181    background-color: red;
    167182    color: white;
  • jobvista-frontend/src/views/static/Header.css

    rb248810 rbefb988  
    66    background-size: contain;
    77    margin-right: 0;
     8
    89}
    910
     
    1819
    1920.navbar {
     21    width: calc(100% - 17px);
    2022    height: 80px;
    21     background-color: #535C91;
     23    background-color: #f8f9fa;
    2224    font-family: Poppins, sans-serif;
     25    position: fixed;
     26    z-index: 10;
     27    box-sizing: border-box;
    2328}
    2429
    2530.navbar .nav-item {
    26 
    2731    color: rgba(1,38,90,0.9);
    2832    margin-right: 15px;
     
    3135    font-size: 20px;
    3236    font-weight: 600;
    33     //font-family: 'Ubuntu', sans-serif;
    3437    font-family: Poppins, sans-serif;
    3538}
     
    3740    color: white;
    3841    background-color: rgba(1,38,90,0.9);
    39     /*border-bottom: 3px solid rgba(1,38,90,0.9);*/
    4042}
    4143.active {
     
    4446}
    4547
    46 .user {
    47 
    48 }
    49 
    50 .auth-box {
    51     display: inline-block;
    52     margin-right: 35px;
    53     margin-left: 5px;
    54     //height: 50px;
    55 
    56 }
    57 
    58 .user {
     48/*NOVO*/
     49
     50:root {
     51    --gray: #555;
     52    --purple: #4e65ff;
     53    --green-blue: #92effd;
     54    --white: #fff;
     55}
     56
     57.navigation {
     58    position: fixed;
     59    top: 10px;
     60    right: 20px;
     61    width: 120px;
     62    height: 60px;
     63    display: flex;
     64    justify-content: space-between;
     65    border-radius: 5px;
     66    background: var(--white);
     67    box-shadow: 0 25px 35px rgba(0, 0, 0, 0.3);
     68    /*box-shadow: 0px 0px 18px 4px rgba(0,0,0,0.46);*/
     69    overflow: hidden;
     70    transition: height 0.5s, width 0.5s;
     71    transition-delay: 0s, 0.3s;
     72    z-index: 15;
     73    /*border: 2px solid var(--gray);*/
     74}
     75
     76.navigation .user-box {
     77    position: relative;
     78    width: 60px;
     79    height: 60px;
     80    display: flex;
     81    align-items: center;
     82    overflow: hidden;
     83    transition: 0.3s;
     84    transition-delay: 0.3s;
     85}
     86
     87.navigation .user-box .username {
     88    font-size: 1.2rem;
     89     font-weight: bold;
     90    white-space: nowrap;
     91    margin: 0;
    5992    color: black;
    6093    padding-top: 10px;
    61     margin-bottom: 0;
    62 
    63 }
    64 
    65 .role {
     94
     95}
     96
     97.navigation .user-box .role {
    6698    color: darkgray;
    6799    margin-bottom: 10px;
    68     font-size: 15px;
    69 
    70 }
    71 
    72 
     100    font-size: 1rem;
     101}
     102
     103.navigation .user-box .image-box {
     104    position: relative;
     105    min-width: 60px;
     106    height: 60px;
     107    background: var(--white);
     108    border-radius: 50%;
     109    overflow: hidden;
     110    border: 10px solid var(--white);
     111
     112}
     113
     114.navigation .user-box .image-box img {
     115    position: absolute;
     116    top: 0;
     117    left: 0;
     118    width: 100%;
     119    height: 100%;
     120    object-fit: cover;
     121    /*border: 1px solid black*/
     122}
     123
     124.navigation .menu-toggle {
     125    position: relative;
     126    width: 60px;
     127    height: 60px;
     128    display: flex;
     129    justify-content: center;
     130    align-items: center;
     131    cursor: pointer;
     132}
     133
     134.navigation .menu-toggle::before {
     135    content: "";
     136    position: absolute;
     137    width: 32px;
     138    height: 2px;
     139    background: var(--gray);
     140    transform: translateY(-10px);
     141    box-shadow: 0 10px var(--gray);
     142    transition: 0.5s;
     143}
     144
     145.navigation .menu-toggle::after {
     146    content: "";
     147    position: absolute;
     148    width: 32px;
     149    height: 2px;
     150    background: var(--gray);
     151    transform: translateY(10px);
     152    transition: 0.5s;
     153}
     154
     155.menu {
     156    position: absolute;
     157    width: 100%;
     158    /*height: calc(100% - 60px);*/
     159    margin-top: 60px;
     160    padding: 0;
     161    border-top: 1px solid rgba(0, 0, 0, 0.1);
     162}
     163
     164.menu .menu-link {
     165    text-decoration: none;
     166    color: black;
     167    display: flex;
     168    align-items: center;
     169    gap: 10px;
     170    font-size: 1.1rem;
     171    padding: 15px 30px;
     172}
     173.menu .menu-link:hover {
     174    background-color: #EEEEEE;
     175}
     176
     177
     178.navigation.active .menu-toggle::before {
     179    transform: translateY(0px) rotate(45deg);
     180    box-shadow: none;
     181}
     182
     183.navigation.active .menu-toggle::after {
     184    transform: translateY(0px) rotate(-45deg);
     185}
     186
     187.navigation.active {
     188    width: 300px;
     189    height: 175px;
     190    transition: width 0.3s, height 0.3s;
     191    transition-delay: 0s, 0.3s;
     192}
     193
     194.navigation.active .user-box {
     195    width: calc(100% - 60px);
     196    transition-delay: 0s;
     197}
     198
     199
     200
     201
     202
  • jobvista-frontend/src/views/static/Header.js

    rb248810 rbefb988  
    11import {Link, NavLink} from "react-router-dom";
    22import "./Header.css"
    3 import { jwtDecode } from "jwt-decode";
     3import {jwtDecode} from "jwt-decode";
    44import {useDispatch, useSelector} from 'react-redux';
    55import {useEffect, useState} from "react";
     
    88import {useNavigate} from "react-router";
    99import {AUTH_TOKEN} from "../../axios/axiosInstance";
     10import {JobSeekerActions} from "../../redux/actions/JobSeekerActions";
     11import {RecruiterActions} from "../../redux/actions/recruiterActions";
    1012
    1113export const Header = (props) => {
     
    1719    const [role, setRole] = useState("");
    1820    const [username, setUsername] = useState("");
     21    const [user, setUser] = useState("");
    1922
    20     const [user, setUser] = useState("");
     23    const [profilePics, setProfilePics] = useState({});
     24    let profilePicState = useSelector(state => state.images.profilePictures);
     25    const [profilePicDispatched, setProfilePicDispatched] = useState(false);
     26
     27    const [logos, setLogos] = useState({});
     28    let logoState = useSelector(state => state.images.logos);
     29    const [logoDispatched, setLogoDispatched] = useState(false);
    2130
    2231    const signOut = () => {
    2332        dispatch(AuthActions.signOut());
    2433        window.location = "/";
     34        //navigator("/")
    2535    }
     36    const [isActive, setIsActive] = useState(false);
     37
     38    const toggleMenu = () => {
     39        setIsActive(!isActive);
     40    };
    2641
    2742    useEffect(() => {
    2843        const token = localStorage.getItem(AUTH_TOKEN);
    29         if (token!=null) {
     44        if (token != null) {
    3045            try {
    3146                const decodedToken = jwtDecode(token);
     
    3449                    role: decodedToken.role,
    3550                    hasAccess: decodedToken.hasAccess,
     51                    id: decodedToken.id
    3652                });
    3753            } catch (error) {
     
    4561            setRole(auth.role);
    4662            setUsername(auth.name);
     63
     64            console.log("ROLE: " + auth.role)
     65
     66            if (auth.role === Roles.JOBSEEKER) {
     67                dispatch(JobSeekerActions.downloadProfilePic(auth.id, (success, reponse) => {
     68                    if (success) {
     69                        setProfilePics(prevState => ({...prevState, [auth.id]: reponse}))
     70                        console.log(reponse)
     71                    }
     72                }))
     73            } else if (auth.role === Roles.RECRUITER) {
     74                dispatch(RecruiterActions.downloadLogo(auth.id, (success, reponse) => {
     75                    if (success) {
     76                        setLogos(prevState => ({...prevState, [auth.id]: reponse}))
     77                        console.log(reponse)
     78                    }
     79                }))
     80            }
    4781        }
    4882    }, [auth]);
     
    5185        <nav className="navbar navbar-expand-lg bg-light">
    5286            <div className="container-fluid">
    53                 <Link to="/" className="logo navbar-brand" />
    54                 <Link to="/" className="brand-name navbar-brand" />
    55                 <div className="collapse navbar-collapse" id="navbarSupportedContent">
     87                <Link to="/" className="logo"/>
     88                <Link to="/" className="brand-name"/>
     89                <div className="navbar-collapse">
    5690                    <ul className="navbar-nav me-auto mb-2 mb-lg-0">
    5791                        <NavLink to="/" className="nav-item nav-link">Home</NavLink>
    58                         {role==Roles.JOBSEEKER &&
     92                        {role == Roles.JOBSEEKER &&
    5993                            <>
    60                                 <NavLink to="/my-applications" className="nav-item nav-link" >My Applications</NavLink>
    61                                 {/*<NavLink to="/favoritejobs" className="nav-item nav-link" >Saved</NavLink>*/}
    62                             </>
    63 
    64                         }
    65                         {role==Roles.RECRUITER &&
    66                             <>
    67                                 <NavLink to="/my-job-advertisements" className="nav-item nav-link" >My Advertisements</NavLink>
    68                                 {/*<NavLink to="/favoritejobs" className="nav-item nav-link" >Saved</NavLink>*/}
     94                                <NavLink to="/my-applications" className="nav-item nav-link">My Applications</NavLink>
    6995                            </>
    7096                        }
    71                         {role===Roles.ADMIN &&
     97                        {role == Roles.RECRUITER &&
     98                            <>
     99                                <NavLink to="/job-management-hub" className="nav-item nav-link">Job Management Hub</NavLink>
     100                            </>
     101                        }
     102                        {role === Roles.ADMIN &&
    72103                            <>
    73104                                <NavLink to="/admin-panel" className="nav-item nav-link">Admin Panel</NavLink>
    74105                            </>
     106                        }
     107                        <NavLink to="/about" className="nav-item nav-link">About Us</NavLink>
     108                        {/*<NavLink to="/contact" className="nav-item nav-link">Support</NavLink>*/}
     109                    </ul>
     110                </div>
     111                {(auth.role === Roles.RECRUITER || auth.role === Roles.ADMIN || auth.role === Roles.JOBSEEKER) ?
     112                    <>
     113                        <div className={`navigation ${isActive ? 'active' : ''}`}>
     114                            <div className="user-box">
     115                                <div className="image-box">
     116                                    {user.role === Roles.JOBSEEKER && <img src={profilePicState[auth.id]} /> }
     117                                    {user.role === Roles.RECRUITER && <img src={logoState[auth.id]} /> }
     118                                    {user.role === Roles.ADMIN && <img src="/images/admin.jpg"/> }
     119                                </div>
     120                                <div className="auth-box">
     121                                    <p className="username">{user.name}</p>
     122                                    {user.role === Roles.RECRUITER && <p className="role">Recruiter</p>}
     123                                    {user.role === Roles.JOBSEEKER && <p className="role">Job Seeker</p>}
     124                                    {user.role === Roles.ADMIN && <p className="role">Admin</p>}
     125                                </div>
    75126
    76                         }
    77                         <NavLink to="/about" className="nav-item nav-link">About</NavLink>
    78                         <NavLink to="/contact" className="nav-item nav-link">Support</NavLink>
    79                     </ul>
     127                            </div>
     128                            <div className="menu-toggle" onClick={toggleMenu}></div>
     129                            <ul className="menu">
     130                                {user.role == Roles.JOBSEEKER &&
     131                                    <>
     132                                        <Link to="/job-seeker/edit-profile" onClick={toggleMenu} className="menu-link">
     133                                            <i className="fa-solid fa-pen-to-square"></i> Edit profile
     134                                        </Link>
     135                                    </>
     136                                }
     137                                {user.role == Roles.RECRUITER &&
     138                                    <>
     139                                        <Link to="/recruiter/edit-profile" onClick={toggleMenu} className="menu-link">
     140                                            <i className="fa-solid fa-pen-to-square"></i> Edit profile
     141                                        </Link>
     142                                    </>
     143                                }
    80144
    81                     {auth ?
    82                         <>
    83                             <img src="/images/user.png" width="45" height="45"/>
    84                             <div className="auth-box">
    85                                 <p className="user"><b>{user.name}</b></p>
    86                                 {user.role==Roles.RECRUITER && <p className="role">Recruiter</p>}
    87                                 {user.role==Roles.JOBSEEKER && <p className="role">Job Seeker</p>}
    88                                 {user.role==Roles.ADMIN && <p className="role">Admin</p>}
    89                                 {/*<p className="role">{user.role==Roles.RECRUITER ? "Recruiter" : "Job Seeker"}</p>*/}
    90                             </div>
    91 
    92 
    93                             <Link onClick={signOut} className="btn auth-secondary-btn">Log out</Link>
    94                         </> :
    95                         <>
    96                             <Link to="/signin" className="btn auth-secondary-btn">Sign in</Link>
    97                         </>
    98                     }
    99 
    100                 </div>
     145                                <Link onClick={signOut} className="menu-link">
     146                                    <i className="fa-solid fa-right-from-bracket"></i> Log out
     147                                </Link>
     148                            </ul>
     149                        </div>
     150                    </> :
     151                    <>
     152                        <Link to="/signin" className="btn auth-secondary-btn">Sign in</Link>
     153                    </>
     154                }
    101155            </div>
    102156        </nav>
Note: See TracChangeset for help on using the changeset viewer.