Changeset befb988
- Timestamp:
- 06/17/24 21:59:14 (5 months ago)
- Branches:
- main
- Children:
- 08f82ec
- Parents:
- b248810
- 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 2 2 3 3 import lombok.RequiredArgsConstructor; 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role; 4 5 import org.springframework.context.annotation.Bean; 5 6 import org.springframework.context.annotation.Configuration; … … 31 32 .authorizeHttpRequests(request -> request 32 33 // 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()) 36 47 .anyRequest().authenticated()) 37 48 .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/AdminController.java
rb248810 rbefb988 2 2 3 3 import lombok.RequiredArgsConstructor; 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO; 4 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 5 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AdminService; … … 20 21 @PostMapping("/change-access/{recruiter_id}") 21 22 public ResponseEntity<?> changeAccess(@PathVariable("recruiter_id") Long recruiterId, @RequestBody boolean access) { 22 Recruiter DetailsDTO recruiterDetailsDTO = adminService.changeAccess(recruiterId, access);23 return new ResponseEntity<>(recruiter DetailsDTO, HttpStatus.OK);23 RecruiterAdminDetailsDTO recruiterAdminDetailsDTO = adminService.changeAccess(recruiterId, access); 24 return new ResponseEntity<>(recruiterAdminDetailsDTO, HttpStatus.OK); 24 25 } 25 26 26 27 @GetMapping("/recruiters") 27 28 public ResponseEntity<?> findAllRecruiters() { 28 List<Recruiter DetailsDTO> recruiterDetailsDTOList = adminService.findAllRecruiters();29 return new ResponseEntity<>(recruiter DetailsDTOList, HttpStatus.OK);29 List<RecruiterAdminDetailsDTO> recruiterAdminDetailsDTOList = adminService.findAllRecruiters(); 30 return new ResponseEntity<>(recruiterAdminDetailsDTOList, HttpStatus.OK); 30 31 } 31 32 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/JobSeekerController.java
rb248810 rbefb988 3 3 4 4 import 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; 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerEditDetailsDTO; 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JobSeekerService; 8 import org.springframework.core.io.Resource; 9 import org.springframework.http.HttpHeaders; 10 import org.springframework.http.HttpStatus; 11 import org.springframework.http.MediaType; 12 import org.springframework.http.ResponseEntity; 13 import org.springframework.web.bind.annotation.*; 14 import org.springframework.web.multipart.MultipartFile; 8 15 9 16 @RestController … … 12 19 @CrossOrigin(origins = "*") 13 20 public 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 } 14 51 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/RecruiterController.java
rb248810 rbefb988 3 3 import lombok.AllArgsConstructor; 4 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 5 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.RecruiterService; 7 import org.springframework.core.io.Resource; 8 import org.springframework.http.HttpHeaders; 6 9 import org.springframework.http.HttpStatus; 10 import org.springframework.http.MediaType; 7 11 import org.springframework.http.ResponseEntity; 8 12 import org.springframework.web.bind.annotation.*; 13 import org.springframework.web.multipart.MultipartFile; 9 14 10 15 @RestController … … 16 21 private final RecruiterService recruiterService; 17 22 18 @GetMapping("/ info/{id}")23 @GetMapping("/{id}/info") 19 24 public ResponseEntity<?> getRecruiterDetailsById(@PathVariable("id") Long id) { 20 25 RecruiterDetailsDTO recruiterDetailsDTO = recruiterService.getRecruiterDetailsById(id); 21 26 return new ResponseEntity<>(recruiterDetailsDTO, HttpStatus.OK); 22 27 } 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 } 23 57 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/Application.java
rb248810 rbefb988 62 62 application.getJobSeeker().getEmail(), 63 63 application.getJobSeeker().getPhoneNumber(), 64 application.getJobAdvertisement().getRecruiter().getId(), 64 65 application.getJobAdvertisement().getRecruiter().getName(), 65 application.getJobAdvertisement().getRecruiter().get Email(),66 application.getJobAdvertisement().getRecruiter().get PhoneNumber(),66 application.getJobAdvertisement().getRecruiter().getContactEmailAddress(), 67 application.getJobAdvertisement().getRecruiter().getContactPhoneNumber(), 67 68 application.getJobAdvertisement().getId(), 68 69 application.getJobAdvertisement().getTitle(), -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationDetailsDTO.java
rb248810 rbefb988 18 18 private String jobSeekerEmail; 19 19 private String jobSeekerPhoneNumber; 20 private Long recruiterId; 20 21 private String recruiterName; 21 22 private String recruiterEmail; -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/job_advertisements/DTO/JobAdvertisementDTO.java
rb248810 rbefb988 11 11 @NoArgsConstructor 12 12 public class JobAdvertisementDTO { 13 private String email;13 private Long id; 14 14 private String title; 15 15 private String description; -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/DTO/RecruiterDetailsDTO.java
rb248810 rbefb988 11 11 @NoArgsConstructor 12 12 public class RecruiterDetailsDTO { 13 private Long id;14 private String email;15 13 private String companyName; 16 14 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; 20 18 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/JobSeeker.java
rb248810 rbefb988 8 8 import lombok.NoArgsConstructor; 9 9 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role; 10 11 import java.nio.file.Paths; 10 12 11 13 @Entity … … 22 24 this.lastName = lastName; 23 25 this.phoneNumber = phoneNumber; 26 27 String relativeProfilePicFilePath = Paths.get("uploads", "job-seekers", "profile-pics", "default-profile-pic.png").toString(); 28 this.profilePicFilePath = relativeProfilePicFilePath; 29 24 30 this.role = Role.ROLE_JOBSEEKER; 25 31 } … … 34 40 private String phoneNumber; 35 41 42 private String profilePicFilePath; 43 36 44 @Override 37 45 public String getName() { -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/Recruiter.java
rb248810 rbefb988 9 9 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role; 10 10 11 import java.nio.file.Paths; 12 11 13 @Entity 12 14 @Data 13 15 @NoArgsConstructor 14 @AllArgsConstructor15 16 @Table(name = "recruiters") 16 17 public class Recruiter extends User { … … 21 22 this.companyName = companyName; 22 23 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; 24 30 this.role = Role.ROLE_RECRUITER; 25 31 } … … 31 37 private String companyDescription; 32 38 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; 35 46 36 47 @Override -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/mappers/JobSeekerMapper.java
rb248810 rbefb988 2 2 3 3 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerDTO; 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JobSeekerEditDetailsDTO; 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 4 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker; 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; 5 8 6 9 public class JobSeekerMapper { … … 26 29 } 27 30 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 28 40 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/users/mappers/RecruiterMapper.java
rb248810 rbefb988 1 1 package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers; 2 2 3 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO; 3 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDTO; 4 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 5 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; 6 8 … … 12 14 recruiter.getPassword(), 13 15 recruiter.getCompanyName(), 14 recruiter.get PhoneNumber()16 recruiter.getContactPhoneNumber() 15 17 ); 16 18 } … … 18 20 public static RecruiterDetailsDTO mapToRecruiterDetailsDTO(Recruiter recruiter) { 19 21 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( 20 42 recruiter.getId(), 21 43 recruiter.getEmail(), 22 44 recruiter.getCompanyName(), 23 45 recruiter.getCompanyDescription(), 24 recruiter.getPhoneNumber(), 46 recruiter.getContactEmailAddress(), 47 recruiter.getContactPhoneNumber(), 48 recruiter.getReceptionist(), 25 49 recruiter.isHasAccess(), 26 50 recruiter.getRegisteredOn() … … 36 60 ); 37 61 } 62 38 63 // Using MapStruct: 39 64 // RecruiterMapper INSTANCE = Mappers.getMapper(RecruiterMapper.class); -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/AdminServiceImpl.java
rb248810 rbefb988 3 3 import lombok.AllArgsConstructor; 4 4 import lombok.RequiredArgsConstructor; 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO; 5 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 6 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; … … 20 21 21 22 @Override 22 public Recruiter DetailsDTO changeAccess(long recruiterId, boolean access) {23 public RecruiterAdminDetailsDTO changeAccess(long recruiterId, boolean access) { 23 24 Recruiter recruiter = recruiterRepository.findById(recruiterId).orElse(null); 24 25 if (recruiter != null) { 25 26 recruiter.setHasAccess(access); 26 27 recruiterRepository.save(recruiter); 27 return RecruiterMapper.mapToRecruiter DetailsDTO(recruiter);28 return RecruiterMapper.mapToRecruiterAdminDetailsDTO(recruiter); 28 29 } 29 30 return null; … … 32 33 33 34 @Override 34 public List<Recruiter DetailsDTO> findAllRecruiters() {35 public List<RecruiterAdminDetailsDTO> findAllRecruiters() { 35 36 List<Recruiter> recruiterList = recruiterRepository.findAll(); 36 return recruiterList.stream().map(RecruiterMapper::mapToRecruiter DetailsDTO).toList();37 return recruiterList.stream().map(RecruiterMapper::mapToRecruiterAdminDetailsDTO).toList(); 37 38 } 38 39 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/JobAdvertisementServiceImpl.java
rb248810 rbefb988 30 30 @Override 31 31 public JobAdDetailsDTO addJobAdvertisement(JobAdvertisementDTO jobAdvertisementDTO) { 32 Recruiter recruiter = recruiterRepository.find RecruiterByEmail(jobAdvertisementDTO.getEmail()).orElseThrow(() -> new IllegalArgumentException("User not found"));32 Recruiter recruiter = recruiterRepository.findById(jobAdvertisementDTO.getId()).orElseThrow(() -> new IllegalArgumentException("User not found")); 33 33 JobAdvertisement jobAdvertisement = new JobAdvertisement( 34 34 recruiter, -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/JwtServiceImpl.java
rb248810 rbefb988 25 25 .claim("role", user.getRole()) 26 26 .claim("access", user.isHasAccess()) 27 .claim("id", user.getId()) 27 28 .setIssuedAt(new Date()) 28 29 .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 3 3 import lombok.RequiredArgsConstructor; 4 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 5 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 5 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; 6 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.RecruiterMapper; 7 8 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.RecruiterRepository; 8 9 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.RecruiterService; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.beans.factory.annotation.Value; 12 import org.springframework.core.io.Resource; 13 import org.springframework.core.io.UrlResource; 9 14 import org.springframework.stereotype.Service; 15 import org.springframework.web.multipart.MultipartFile; 16 17 import java.io.IOException; 18 import java.nio.file.Files; 19 import java.nio.file.Path; 20 import java.nio.file.Paths; 21 import java.nio.file.StandardCopyOption; 10 22 11 23 @Service … … 14 26 15 27 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 16 62 17 63 @Override … … 20 66 return RecruiterMapper.mapToRecruiterDetailsDTO(recruiter); 21 67 } 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 22 115 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/AdminService.java
rb248810 rbefb988 1 1 package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef; 2 2 3 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterAdminDetailsDTO; 3 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 4 5 … … 6 7 7 8 public interface AdminService { 8 Recruiter DetailsDTO changeAccess(long recruiterId, boolean access);9 List<Recruiter DetailsDTO> findAllRecruiters();9 RecruiterAdminDetailsDTO changeAccess(long recruiterId, boolean access); 10 List<RecruiterAdminDetailsDTO> findAllRecruiters(); 10 11 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/RecruiterService.java
rb248810 rbefb988 2 2 3 3 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterDetailsDTO; 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; 4 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.RecruiterEditDetailsDTO; 5 import org.springframework.core.io.Resource; 6 import org.springframework.web.multipart.MultipartFile; 5 7 6 8 public interface RecruiterService { 7 9 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); 8 15 } -
jobvista-frontend/package-lock.json
rb248810 rbefb988 32 32 "react-scripts": "5.0.1", 33 33 "react-select": "^5.8.0", 34 "react-toastify": "^10.0.5", 34 35 "redux": "^5.0.1", 35 36 "redux-thunk": "^3.1.0", … … 16027 16028 } 16028 16029 }, 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 }, 16029 16042 "node_modules/react-transition-group": { 16030 16043 "version": "4.4.5", -
jobvista-frontend/package.json
rb248810 rbefb988 27 27 "react-scripts": "5.0.1", 28 28 "react-select": "^5.8.0", 29 "react-toastify": "^10.0.5", 29 30 "redux": "^5.0.1", 30 31 "redux-thunk": "^3.1.0", -
jobvista-frontend/src/App.css
rb248810 rbefb988 2 2 @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'); 3 3 4 * { 5 margin: 0; 6 padding: 0; 7 box-sizing: border-box; 8 } 9 10 html { 11 scroll-behavior: smooth; 12 } 13 4 14 .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;*/ 7 20 height: 100vh; 8 21 overflow-y: auto; … … 14 27 -webkit-box-pack: center; 15 28 justify-content: center; 16 margin-bottom: 20px;29 margin-bottom: 20px; 17 30 margin-top: 20px; 18 31 } … … 36 49 } 37 50 51 .container, .custom-container { 52 margin-top: 120px !important; 53 } 54 55 .no-additional-margin { 56 margin-top: 80px !important; 57 } 58 38 59 39 60 /*font-family: 'Ubuntu', sans-serif;*/ … … 49 70 height: 280px !important; 50 71 } 72 73 .container .g-4 { 74 --bs-gutter-y: 6rem !important; 75 margin-bottom: 100px !important; 76 } 77 51 78 52 79 .add-new-card { … … 80 107 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 81 108 transform: translate(0, 0); 82 height: 260px;109 height: auto; 83 110 } 84 111 … … 167 194 border-radius: 8px; 168 195 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; 173 198 font-weight: bold; 174 199 padding: 5px 10px; 175 200 transition: 0.2s; 201 text-decoration: none; 202 text-align: center; 176 203 } 177 204 … … 180 207 } 181 208 .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 4 4 import {BrowserRouter} from "react-router-dom"; 5 5 import {Header} from "./views/static/Header"; 6 import {RoutesConfig}from "./auth/RoutesConfig";7 import {useEffect, useState} from "react";6 import RoutesConfig from "./auth/RoutesConfig"; 7 import React, {useEffect, useState} from "react"; 8 8 import {AuthActions} from "./redux/actions/authActions"; 9 9 import {AUTH_TOKEN} from "./axios/axiosInstance"; 10 10 import {jwtDecode} from "jwt-decode"; 11 11 import {NoAccess} from "./views/static/NoAccess"; 12 import {Loading} from "./views/static/Loading"; 13 import {ToastContainer} from "react-toastify"; 12 14 13 15 function App() { … … 20 22 const [user, setUser] = useState(null); 21 23 const [loading, setLoading] = useState(true); 24 const [minimumLoadingTime, setMinimumLoadingTime] = useState(true); 25 22 26 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 }, []); 23 37 24 38 useEffect(() => { … … 30 44 name: decodedToken.name, 31 45 role: decodedToken.role, 32 hasAccess: auth.currentUser.access, 46 hasAccess: decodedToken.access, 47 id: decodedToken.id 33 48 }); 34 49 setLoading(false); … … 42 57 }, [auth]); 43 58 44 if (loading ) {45 return < NoAccess/>; // Replace LoadingSpinner with your loading indicator component59 if (loading || minimumLoadingTime) { 60 return <Loading />; // Replace LoadingSpinner with your loading indicator component 46 61 } 47 62 … … 58 73 <Header /> 59 74 <RoutesConfig /> 75 {/*<Loading/>*/} 60 76 </> 61 77 ) : ( 62 78 <NoAccess user={user}/> 63 79 )} 64 65 66 80 </BrowserRouter> 81 <ToastContainer/> 67 82 </div> 68 83 ); -
jobvista-frontend/src/auth/RoutesConfig.js
rb248810 rbefb988 1 2 import {Route, Router, Routes} from 'react-router-dom' 1 import {Navigate, Route, Router, Routes} from 'react-router-dom' 3 2 import {Dashboard} from "../views/dashboard/Dashboard"; 4 3 import {SignInForm} from "../views/auth/SignInForm"; 5 4 import {SignUpRecruiterForm} from "../views/auth/SignUpRecruiterForm"; 6 5 import {SignUpJobSeekerForm} from "../views/auth/SignUpJobSeekerForm"; 7 import { JobAdvertisements} from "../views/job_advertisements/JobAdvertisements";6 import {Workspace} from "../views/job_advertisements/RecruiterWorkspace"; 8 7 import {JobAdDetails} from "../views/job_advertisements/JobAdDetails"; 9 8 import {ApplicationsByJobAd} from "../views/applications/ApplicationsByJobAd"; 10 9 import {ApplicationsByJobSeeker} from "../views/applications/ApplicationsByJobSeeker"; 10 11 import {AdminPanel} from "../views/admin_panel/AdminPanel"; 12 import {RecruiterProfile} from "../views/recruiters/RecruiterProfile"; 13 import {AboutUs} from "../views/static/AboutUs"; 14 import {JobSeekerEditProfile} from "../views/edit_profile/JobSeekerEditProfile"; 15 import {RecruiterEditProfile} from "../views/edit_profile/RecruiterEditProfile"; 16 import Roles from "../enumerations/Roles"; 17 import {useSelector} from "react-redux"; 11 18 import {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 = () => { 19 import {ErrorPage} from "../views/static/ErrorPage"; 20 21 export 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 79 export 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 164 const AllRoutes = [...PrivateRoutes, ...PublicRoutes]; 165 166 const filterRoutes = (roleParam) => { 167 return AllRoutes.filter(route => { 168 return route.permission.includes(roleParam); 169 }); 170 }; 171 const 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]); 17 180 18 181 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> 30 209 ) 31 210 } 211 212 export default RoutesConfig -
jobvista-frontend/src/enumerations/Roles.js
rb248810 rbefb988 14 14 RECRUITER: 'ROLE_RECRUITER', 15 15 JOBSEEKER: 'ROLE_JOBSEEKER', 16 GUEST: 'ROLE_GUEST' 16 17 }; -
jobvista-frontend/src/redux/actionTypes.js
rb248810 rbefb988 20 20 export const FETCH_RECRUITERS = "FETCH_RECRUITERS" 21 21 export const CHANGE_ACCESS = "CHANGE_ACCESS" 22 export const SUBMIT_RECRUITER_LOGO = "SUBMIT_RECRUITER_LOGO" 23 export const SUBMIT_RECRUITER_COVER = "SUBMIT_RECRUITER_COVER" 24 export const SET_LOGO_URL = 'SET_LOGO_URL'; 25 export const SET_PROFILE_PIC_URL = 'SET_PROFILE_PIC_URL'; 22 26 23 -
jobvista-frontend/src/redux/actions/applicationActions.js
rb248810 rbefb988 47 47 } 48 48 }, 49 fetchApplicationsByJobSeeker: ( callback) => {49 fetchApplicationsByJobSeeker: (jobSeekerId, callback) => { 50 50 return dispatch => { 51 let currentUser = JSON.parse(localStorage.getItem(CURRENT_USER)); 52 axios.get("/my-applications/" + currentUser.id) 51 axios.get("/my-applications/" + jobSeekerId) 53 52 .then(response => { 54 53 dispatch({ -
jobvista-frontend/src/redux/actions/authActions.js
rb248810 rbefb988 45 45 //const refreshToken = response.refreshToken; 46 46 const user = { 47 id: response.id,48 email: response.email,49 47 name: response.name, 50 48 role: response.role, 51 49 access: response.hasAccess, 50 id: response.id, 52 51 }; 53 52 dispatch({ -
jobvista-frontend/src/redux/actions/jobAdvertisementActions.js
rb248810 rbefb988 87 87 }, 88 88 89 fetchJobAdvertisementsByRecruiter: ( callback) => {89 fetchJobAdvertisementsByRecruiter: (recruiterId, callback) => { 90 90 return dispatch => { 91 91 let currentUser = JSON.parse(localStorage.getItem(CURRENT_USER)); 92 axios.get("/job-advertisements/recruiter/" + currentUser.id)92 axios.get("/job-advertisements/recruiter/" + recruiterId) 93 93 .then(response => { 94 94 dispatch({ … … 103 103 }, 104 104 105 filterJobAdvertisementsByRecruiter: ( filter, callback) => {105 filterJobAdvertisementsByRecruiter: (id, filter, callback) => { 106 106 107 107 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) 109 109 .then(response => { 110 110 callback(true, response) … … 116 116 117 117 fetchRecruiterDetailsById: (id, callback) => { 118 axios.get("/recruiter/ info/" + id)118 axios.get("/recruiter/"+id+"/info") 119 119 .then(response => { 120 120 callback(true, response) -
jobvista-frontend/src/redux/reducers/authReducer.js
rb248810 rbefb988 3 3 import {isExpired} from "react-jwt"; 4 4 import {CURRENT_USER, SIGN_IN, SIGN_OUT, UPDATE_TOKEN} from "../actionTypes"; 5 import {jwtDecode} from "jwt-decode"; 5 6 6 7 const initialState = { … … 13 14 case SIGN_IN: 14 15 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)); 16 17 return { 17 18 ...state, … … 21 22 case UPDATE_TOKEN: 22 23 let token = action.payload; 24 let decodedToken; 23 25 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 24 34 if(!isExpired(token)) { 25 35 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 }; 27 42 } else { 28 localStorage.removeItem(CURRENT_USER);43 //localStorage.removeItem(CURRENT_USER); 29 44 localStorage.removeItem(AUTH_TOKEN); 30 45 currentUser = ""; … … 37 52 }; 38 53 case SIGN_OUT: 39 localStorage.removeItem(CURRENT_USER);54 //localStorage.removeItem(CURRENT_USER); 40 55 localStorage.removeItem(AUTH_TOKEN); 41 56 return { -
jobvista-frontend/src/redux/reducers/jobAdvertisementReducer.js
rb248810 rbefb988 1 1 import { 2 2 ADD_JOB_ADVERTISEMENT, 3 CURRENT_USER, DELETE_JOB_ADVERTISEMENT, EDIT_JOB_ADVERTISEMENT, 3 CURRENT_USER, 4 DELETE_JOB_ADVERTISEMENT, 5 EDIT_JOB_ADVERTISEMENT, 4 6 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 6 11 } from "../actionTypes"; 7 12 import {sortElementsBy} from "../../utils/utils"; -
jobvista-frontend/src/redux/store.js
rb248810 rbefb988 6 6 import adminReducer from "./reducers/adminReducer" 7 7 import {AdminActions} from "./actions/adminActions"; 8 import ImagesReducer from "./reducers/imagesReducer"; 8 9 9 10 // const rootReducer = combineReducers({ … … 21 22 jobAd: jobAdReducer, 22 23 appl: applicationReducer, 23 admin: adminReducer 24 admin: adminReducer, 25 images: ImagesReducer 24 26 }, 25 27 }); -
jobvista-frontend/src/views/admin_panel/AdminPanel.js
rb248810 rbefb988 4 4 import {useState, useEffect} from "react"; 5 5 import {AdminActions} from "../../redux/actions/adminActions"; 6 import {notifyAccessUpdate} from "../../utils/toastUtils"; 6 7 7 8 export const AdminPanel = () => { … … 24 25 setRecruiters(recruitersState) 25 26 console.log("Fetch all recruiters STATE") 27 console.log(recruitersState) 28 26 29 } 27 30 }, [recruitersState]) … … 32 35 }; 33 36 34 const handleAccessChange = (recruiterId, newAccessStatus) => { 37 const handleAccessChange = (recruiterId, companyName, newAccessStatus) => { 38 35 39 setRecruiters(prevState => 36 40 prevState.map(recruiter => … … 41 45 ); 42 46 43 console.log(recruiterId + " " + newAccessStatus)44 45 47 dispatch(AdminActions.changeAccess(recruiterId, newAccessStatus, (success, response) => { 46 48 if(success) { 47 console.log("Access changed")49 notifyAccessUpdate(companyName) 48 50 } 49 51 })); … … 51 53 52 54 return ( 53 <div className=" applications-container mt-5">55 <div className="custom-container mt-5"> 54 56 <table className="table table-striped"> 55 57 <thead> … … 64 66 </thead> 65 67 <tbody> 66 {recruiters .map((recruiter) => (68 {recruitersState.map((recruiter) => ( 67 69 <tr key={recruiter.id}> 68 70 <th scope="row">{recruiter.id}</th> … … 70 72 <td>{recruiter.email}</td> 71 73 <td>{recruiter.companyName}</td> 72 <td>{recruiter. phoneNumber}</td>74 <td>{recruiter.contactPhoneNumber}</td> 73 75 <td> 74 76 <label className="switch"> … … 76 78 type="checkbox" 77 79 checked={recruiter.hasAccess} 78 onChange={(e) => handleAccessChange(recruiter.id, e.target.checked)}80 onChange={(e) => handleAccessChange(recruiter.id, recruiter.companyName, e.target.checked)} 79 81 /> 80 82 <span className="slider"></span> -
jobvista-frontend/src/views/applications/ApplicationDetailsModal.js
rb248810 rbefb988 1 1 import React, {useEffect, useState} from "react"; 2 import "../ job_advertisements/Form.css";2 import "../shared_css/Modal.css"; 3 3 4 4 import 'react-responsive-modal/styles.css'; … … 19 19 20 20 21 export const ViewApplicationDetails= (props) => {21 export const ApplicationDetailsModal = (props) => { 22 22 const {application} = props 23 23 const [modal, setModal] = useState(false); … … 54 54 <div className="col-md-6"> 55 55 <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="applic taion-textarea"/>56 <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled placeholder="Write your answer here..." className="application-textarea"/> 57 57 58 58 59 59 <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="applic taion-textarea"/>60 <textarea disabled type="text" defaultValue={application.questionAnswers[1]} placeholder="Write your answer here..." className="application-textarea"/> 61 61 62 62 63 63 <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="applic taion-textarea"/>64 <textarea disabled type="text" defaultValue={application.questionAnswers[2]} placeholder="Write your answer here..." className="application-textarea"/> 65 65 66 66 </div> … … 73 73 <br/> 74 74 <label className="label">Message to the recruiter</label> 75 <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." className="applic taion-textarea"/>75 <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." className="application-textarea"/> 76 76 77 77 </div> -
jobvista-frontend/src/views/applications/Applications.css
rb248810 rbefb988 1 . applications-container {1 .custom-container { 2 2 width: 65% !important; 3 3 max-width: 1500px !important; … … 6 6 7 7 .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; 11 11 display: flex; 12 justify-content: center; 12 justify-content: start; 13 border-bottom: 1px solid #CFCFCF; 13 14 } 14 .application-title h 1{15 font-weight: bold;15 .application-title h3 { 16 font-weight: 500; 16 17 } 17 18 … … 28 29 transition: all 0.3s ease; 29 30 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 30 transform: translate(0, 0);31 31 height: auto; 32 32 padding: 20px 20px; 33 33 display: flex; 34 34 justify-content: center; 35 gap: 20px; 35 36 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%; 36 45 } 37 46 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; 41 54 } 42 55 43 .application-card . left-box.jobAd-title {56 .application-card .app-info .jobAd-title { 44 57 font-weight: 600; 45 58 /*text-transform: uppercase;*/ 46 59 font-family: 'Segoe UI', sans-serif; 47 font-size: 22px;60 /*font-size: 22px;*/ 48 61 } 62 .application-card .app-info .jobAd-title 49 63 50 .application-card . left-box.contact-info {64 .application-card .app-info .contact-info { 51 65 display: inline-flex; 52 gap: 10px;66 gap: 30px; 53 67 } 54 .application-card . left-box.contact-info .contact-item {68 .application-card .app-info .contact-info .contact-item { 55 69 display: inline-flex; 56 70 align-items: center; 57 71 gap: 5px; 72 margin-right: 10px 58 73 } 59 74 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%; 62 81 display: inline-flex; 63 82 justify-content: end; … … 66 85 } 67 86 68 .application-card . right-box.select {69 width: 35%;87 .application-card .app-status .select { 88 width: 45%; 70 89 display: inline-block; 71 90 } 91 92 .status { 93 color: white; 94 padding: 5px 10px; 95 border-radius: 5px; 96 width: 150px; 97 text-align: center; 98 } 99 72 100 73 101 .application-button { … … 75 103 border-radius: 8px; 76 104 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; 82 111 } 112 83 113 .application-button:hover { 84 background-color: rgb(187, 215, 235); 114 background: linear-gradient(to bottom, #bbbbbb, #dddddd); 115 color: #333333; 85 116 } 86 117 -
jobvista-frontend/src/views/applications/ApplicationsByJobAd.js
rb248810 rbefb988 1 import {useDispatch } from "react-redux";1 import {useDispatch, useSelector} from "react-redux"; 2 2 import {useEffect, useState} from "react"; 3 3 import {useParams} from "react-router"; 4 4 import {ApplicationActions} from "../../redux/actions/applicationActions"; 5 import { ViewApplicationDetails} from "./ViewApplicationDetails";5 import {ApplicationDetailsModal} from "./ApplicationDetailsModal"; 6 6 import "./Applications.css" 7 7 import Select from "react-select"; 8 import {sortElementsBy} from "../../utils/utils"; 9 import {JobSeekerActions} from "../../redux/actions/JobSeekerActions"; 10 import {notifyAppStatusUpdate} from "../../utils/toastUtils"; 8 11 9 12 export const ApplicationsByJobAd = () => { … … 11 14 const dispatch = useDispatch(); 12 15 let {advertisement_id} = useParams(); 16 13 17 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 14 25 const [jobAdTitle, setJobAdTitle] = useState(""); 15 26 16 27 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})) 21 66 } 22 67 })) 23 }, []) 68 } 69 24 70 25 71 const options = [ … … 37 83 dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => { 38 84 if(success) { 39 console.log("Status updated.") 85 // console.log("Status updated.") 86 notifyAppStatusUpdate() 40 87 } 41 88 })) … … 43 90 44 91 45 return (<div className=" applications-container">92 return (<div className="custom-container"> 46 93 <div className="application-title"> 47 94 {jobAdTitle ? 48 <h 1>Applications for <span>{jobAdTitle}</span></h1> :95 <h3>Applications for <b>{jobAdTitle}</b></h3> : 49 96 <h1></h1> 50 97 } 51 52 98 </div> 53 99 54 100 {applicationsByJobAd && applicationsByJobAd.map((application, index) => ( 55 101 <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"> 57 111 <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 58 112 day: 'numeric', … … 60 114 year: 'numeric' 61 115 })}</b></span> 62 <br/><br/>63 116 <div className="contact-info"> 64 117 <div className="contact-item"> 65 118 <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span> 66 </div> 67 <div className="contact-item"> 119 </div> <div className="contact-item"> 68 120 <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span> 69 </div> 70 <div className="contact-item"> 121 </div> <div className="contact-item"> 71 122 <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span> 72 123 </div> … … 74 125 </div> 75 126 76 <div className=" right-box">77 < ViewApplicationDetailsapplication={application}/>127 <div className="app-status"> 128 <ApplicationDetailsModal application={application}/> 78 129 <div className="select"> 79 130 <Select options={options} onChange={(selectedOption) => handleChangeStatus(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)}/> -
jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js
rb248810 rbefb988 2 2 import {useEffect, useState} from "react"; 3 3 import {ApplicationActions} from "../../redux/actions/applicationActions"; 4 import { ViewApplicationDetails} from "./ViewApplicationDetails";4 import {ApplicationDetailsModal} from "./ApplicationDetailsModal"; 5 5 import Select from "react-select"; 6 7 import {RecruiterActions} from "../../redux/actions/recruiterActions"; 8 import {sortElementsBy} from "../../utils/utils"; 6 9 7 10 export const ApplicationsByJobSeeker = () => { 8 11 const dispatch = useDispatch(); 12 const auth = useSelector(state => state.auth.currentUser); 13 9 14 const [applicationsByJobSeeker, setApplicationsByJobSeeker] = useState([]); 10 15 let applicationsByJobSeekerState = useSelector(state => state.appl.applicationsByJobSeeker); 11 16 const [dispatched, setDispatched] = useState(false); 12 17 18 const [logos, setLogos] = useState({}); 19 let logosState = useSelector(state => state.images.logos) 20 const [logoDispatched, setLogoDispatched] = useState(false); 21 22 23 13 24 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) => { 16 27 if(success && response.data.length > 0) { 17 setApplicationsByJobSeeker( response.data);28 setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn")); 18 29 } 30 setDispatched(true) 19 31 console.log("Fetch applications by job seeker GET") 20 32 })) 21 setDispatched(true) 33 22 34 } else { 23 setApplicationsByJobSeeker( applicationsByJobSeekerState);35 setApplicationsByJobSeeker(sortElementsBy(applicationsByJobSeekerState, "submittedOn")); 24 36 console.log("Fetch applications by job seeker STATE") 25 37 } 26 38 }, [applicationsByJobSeekerState]) 27 39 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 28 68 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>} 33 73 ]; 34 74 … … 40 80 41 81 return ( 42 <div className="applications-container"> 82 <div className="custom-container"> 83 43 84 <div className="application-title"> 44 85 <h3>Application history</h3> 45 86 </div> 46 87 {applicationsByJobSeeker && applicationsByJobSeeker.map((application, index) => ( 47 88 <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> 52 100 <div className="contact-info"> 53 101 <div className="contact-item"> … … 68 116 </div> 69 117 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>*/} 75 124 76 125 </div> -
jobvista-frontend/src/views/applications/ApplyToJobAdModal.js
rb248810 rbefb988 1 1 import React, {useState} from "react"; 2 import "../ job_advertisements/Form.css";2 import "../shared_css/Modal.css"; 3 3 4 4 import 'react-responsive-modal/styles.css'; 5 5 import {Modal} from 'react-responsive-modal'; 6 import Select from "react-select";7 6 8 7 //Validation … … 12 11 13 12 14 import {employmentStatusOptions, industryOptions, jobTypeOptions} from "../selectOptions";15 13 import {useDispatch, useSelector} from "react-redux"; 16 import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions";17 14 import Roles from "../../enumerations/Roles"; 18 15 import {ApplicationActions} from "../../redux/actions/applicationActions"; 16 import {notifyJobAdApply} from "../../utils/toastUtils"; 19 17 20 18 … … 55 53 formData,(success, response) => { 56 54 if(success) { 57 console.log("Job Advertisement added")58 55 toggleModal() 56 notifyJobAdApply() 59 57 } 60 58 } … … 70 68 {role===Roles.JOBSEEKER && 71 69 <> 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> } 73 71 {!jobAd.active && <button className="card-button apply disabled">Apply now</button> } 74 72 </> … … 86 84 <label className="label">Why are you interested in joining our company?</label> 87 85 <textarea type="text" placeholder="Write your answer here..." 88 {...register("answerOne")} className="applic taion-textarea"/>86 {...register("answerOne")} className="application-textarea"/> 89 87 <p style={{color: "red"}}>{errors.answerOne?.message}</p> 90 88 91 89 <label className="label">What makes you a good fit for this position?</label> 92 90 <textarea type="text" placeholder="Write your answer here..." 93 {...register("answerTwo")} className="applic taion-textarea"/>91 {...register("answerTwo")} className="application-textarea"/> 94 92 <p style={{color: "red"}}>{errors.answerTwo?.message}</p> 95 93 96 94 <label className="label">What do you hope to achieve in your first 6 months in this role?</label> 97 95 <textarea type="text" placeholder="Write your answer here..." 98 {...register("answerThree")} className="applic taion-textarea"/>96 {...register("answerThree")} className="application-textarea"/> 99 97 <p style={{color: "red"}}>{errors.answerThree?.message}</p> 100 98 … … 103 101 <label htmlFor="start">Curriculum vitae (CV)</label> 104 102 <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"/> 106 104 107 105 <br/> 108 106 <label className="label">Message to the recruiter</label> 109 107 <textarea type="text" placeholder="Optional..." 110 {...register("messageToRecruiter")} className="applic taion-textarea"/>108 {...register("messageToRecruiter")} className="application-textarea"/> 111 109 112 110 <br/><br/> … … 115 113 </div> 116 114 117 <div className="aligned"> 115 <div className="modal-buttons"> 116 <div className="cancel-btn" onClick={toggleModal}> Cancel</div> 118 117 <button className="submit-btn"> Submit</button> 119 118 </div> -
jobvista-frontend/src/views/auth/SignInForm.js
rb248810 rbefb988 8 8 import {yupResolver} from "@hookform/resolvers/yup"; 9 9 import {AuthActions} from "../../redux/actions/authActions"; 10 import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils"; 10 11 11 12 export const SignInForm = () => { … … 27 28 const signIn = async (values) => { 28 29 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 } 37 36 })); 38 37 } catch (err) { 39 console.error(err);38 // console.error(err); 40 39 } 41 40 } -
jobvista-frontend/src/views/dashboard/Dashboard.js
rb248810 rbefb988 1 import ". /Dashboard.css"1 import "../shared_css/Random.css" 2 2 3 3 import {useDispatch, useSelector} from "react-redux"; … … 12 12 import {AUTH_TOKEN} from "../../axios/axiosInstance"; 13 13 import {jwtDecode} from "jwt-decode"; 14 15 export const Dashboard = (props) => { 14 import {RecruiterActions} from "../../redux/actions/recruiterActions"; 15 16 export const Dashboard = () => { 16 17 17 18 const dispatch = useDispatch(); … … 19 20 const [jobAdvertisements, setJobAdvertisements] = useState([]); 20 21 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 21 28 const auth = useSelector(state => state.auth); 22 29 … … 25 32 const [selectedIndustry, setSelectedIndustry] = useState("all"); 26 33 const [searchTerm, setSearchTerm] = useState(""); 27 const [dispatched, setDispatched] = useState(false) 34 35 28 36 29 37 // const [user, setUser] = useState(null); … … 54 62 55 63 useEffect(() => { 56 if (! dispatched && jobAdvertisementsState.length == 0) {64 if (!jobDispatched && jobAdvertisementsState.length == 0) { 57 65 dispatch(JobAdvertisementActions.fetchJobAdvertisements((success, response) => { 58 66 if (success && response.data.length > 0) { 59 setJobAdvertisements(sortElementsBy(response.data ))67 setJobAdvertisements(sortElementsBy(response.data, "postedOn")) 60 68 } 61 set Dispatched(true)69 setJobDispatched(true) 62 70 console.log("Fetch all job advertisements GET") 63 71 })) … … 66 74 setJobAdvertisements(jobAdvertisementsState) 67 75 console.log("Fetch all job advertisements STATE") 76 77 68 78 } 69 79 }, [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 }; 70 105 71 106 let filterJobAdvertisements = () => { … … 86 121 87 122 <div className="container"> 88 <div className=" head-dashboard-box">123 <div className="filter-container"> 89 124 <div className="row"> 90 <div className="col-md-12 filter- container">125 <div className="col-md-12 filter-box"> 91 126 <div className="search-container"> 92 127 <i className="fa-solid fa-magnifying-glass search-icon"></i> … … 117 152 /> 118 153 </div> 119 <button onClick={filterJobAdvertisements} className="b tn-open-modal">Find jobs</button>154 <button onClick={filterJobAdvertisements} className="blue-submit-button">Find jobs</button> 120 155 </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>*/}133 156 </div> 134 157 </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"> 137 159 {jobAdvertisements && 138 160 jobAdvertisements.map((jobAd, index) => ( … … 146 168 </div> 147 169 <div className="card-body"> 170 <img 171 // loading gif 172 src={logos[jobAd.recruiterId]} 173 alt="" 174 width={100} height={100} 175 /> 148 176 <h5 className="card-title">{jobAd.title}</h5> 149 177 <span>{jobAd.industry} • <span style={{ -
jobvista-frontend/src/views/job_advertisements/AddJobAdModal.js
rb248810 rbefb988 1 1 import React, {useState} from "react"; 2 import ". /Form.css";2 import "../shared_css/Modal.css"; 3 3 4 4 import { Editor } from 'primereact/editor'; … … 17 17 import {useDispatch, useSelector} from "react-redux"; 18 18 import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions"; 19 import {notifyJobAdPost} from "../../utils/toastUtils"; 19 20 20 21 … … 32 33 description: yup.string().required("Please enter a description"), 33 34 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"), 35 36 jobType: yup.mixed().required("Select job type"), 36 37 employmentStatus: yup.mixed().required("Select employment status"), … … 46 47 dispatch(JobAdvertisementActions.addJobAdvertisement( 47 48 { 48 email: auth.email,49 id: auth.id, 49 50 title: values.title, 50 51 description: values.description, … … 56 57 }, (success, response) => { 57 58 if (success) { 58 console.log("Job Advertisement added")59 // console.log("Job Advertisement added") 59 60 toggleModal() 61 notifyJobAdPost() 60 62 } 61 63 } … … 75 77 </div> 76 78 {/*<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> 78 80 <div className="head-modal"> 79 81 <h3>Post Job Advertisement</h3> … … 82 84 83 85 <div className="modal-content"> 84 <form onSubmit={handleSubmit(addJobAdvertisement)}>86 <form> 85 87 <div className="row"> 86 88 <div className="col-md-7"> … … 111 113 <div className="col-md-5"> 112 114 <label className="label">Hourly rate:</label> 113 <input {...register("startingSalary")}/>115 <input type="number" {...register("startingSalary")}/> 114 116 <p style={{color: "red"}}>{errors.startingSalary?.message}</p> 115 117 … … 157 159 </div> 158 160 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> 161 164 </div> 162 165 -
jobvista-frontend/src/views/job_advertisements/DeleteJobAdModal.js
rb248810 rbefb988 1 1 import React, {useState} from "react"; 2 import ". /Form.css";2 import "../shared_css/Modal.css"; 3 3 4 4 import 'react-responsive-modal/styles.css'; … … 15 15 import {useDispatch, useSelector} from "react-redux"; 16 16 import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions"; 17 import {notifyJobAdDelete} from "../../utils/toastUtils"; 17 18 18 19 … … 29 30 dispatch(JobAdvertisementActions.deleteJobAdvertisement(jobAd.props.id, (success, response) => { 30 31 if (success) { 31 console.log("Job Advertisement deleted")32 // console.log("Job Advertisement deleted") 32 33 toggleModal() 34 notifyJobAdDelete() 33 35 } 34 36 })) -
jobvista-frontend/src/views/job_advertisements/EditJobAdModal.js
rb248810 rbefb988 1 1 import React, {useEffect, useState} from "react"; 2 import ". /Form.css";2 import "../shared_css/Modal.css"; 3 3 4 4 import 'react-responsive-modal/styles.css'; … … 16 16 import {JobAdvertisementActions} from "../../redux/actions/jobAdvertisementActions"; 17 17 import {Editor} from "primereact/editor"; 18 import {notifyJobAdDelete, notifyJobAdEdit} from "../../utils/toastUtils"; 18 19 19 20 … … 23 24 const auth = useSelector(state => state.auth.currentUser) 24 25 const toggleModal = () => { 26 console.log("NAD SET") 25 27 setModal(!modal); 28 console.log("POD SET") 29 26 30 }; 27 31 … … 52 56 }, jobAd.props.id, (success, response) => { 53 57 if(success) { 54 console.log("Job Advertisement edited")58 // console.log("Job Advertisement edited") 55 59 toggleModal() 60 notifyJobAdEdit() 56 61 } 57 62 } … … 78 83 79 84 <div className="modal-content"> 80 <form onSubmit={handleSubmit(editJobAdvertisement)}>85 <form> 81 86 <div className="row"> 82 87 <div className="col-md-7"> … … 92 97 name="description" 93 98 control={control} 99 defaultValue={jobAd.props.description} 94 100 render={({ field }) => ( 95 101 <Editor 96 defaultValue={jobAd.props.description}102 // defaultValue={jobAd.props.description} 97 103 value={jobAd.props.description} 98 104 onTextChange={(e) => field.onChange(e.htmlValue)} … … 107 113 <div className="col-md-5"> 108 114 <label className="label">Hourly rate:</label> 109 <input defaultValue={jobAd.props.startingSalary} {...register("startingSalary")}/>115 <input type="number" defaultValue={jobAd.props.startingSalary} {...register("startingSalary")}/> 110 116 <p style={{color: "red"}}>{errors.startingSalary?.message}</p> 111 117 … … 166 172 </div> 167 173 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> 170 177 </div> 171 172 178 </form> 173 179 </div> -
jobvista-frontend/src/views/job_advertisements/JobAdDetails.css
rb248810 rbefb988 10 10 .details-wrap { 11 11 width: 100%; 12 //height: auto;13 height: 80vh;12 height: auto; 13 max-height: 80vh; 14 14 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 15 34 background-color: #fff; 16 35 border-radius: 12px; … … 56 75 } 57 76 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 58 98 .apply:not(.expired) { 59 99 width: 20% !important; … … 65 105 background-position: right center; 66 106 } 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 10 10 import {AddJobAdModal} from "./AddJobAdModal"; 11 11 import {ApplyToJobAdModal} from "../applications/ApplyToJobAdModal"; 12 import {Link} from "react-router-dom"; 13 import {RecruiterActions} from "../../redux/actions/recruiterActions"; 12 14 13 15 … … 20 22 const auth = useSelector(state => state.auth.currentUser); 21 23 24 let logosState = useSelector(state => state.images.logos) 25 const [logoDispatched, setLogoDispatched] = useState(false) 26 const [logoView, setLogoView] = useState(null); 27 22 28 useEffect(() => { 23 29 setRole(auth.role) 24 30 }, [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]) 25 48 26 49 … … 41 64 <div className="col-md-9"> 42 65 <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> 43 73 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> 48 96 </div> 49 97 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>57 98 58 99 <h4>About the job</h4> … … 66 107 <div className="col-md-3"> 67 108 <div className="details-wrap"> 68 < h3>{jobAd.recruiterName}</h3>109 <Link className="recruiter-link" to={`/recruiters/${jobAd.recruiterId}`}>{jobAd.recruiterName} </Link> 69 110 70 {/*TO DO - AFTER IMPLEMENTING FORM FOR UPDATING PERSONAL INFO*/}71 111 <h4>About the company</h4> 72 112 <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> 76 120 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> </>} 77 125 </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> 81 130 </div> 82 131 </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 2 2 3 3 import "./JobAdvertisements.css" 4 import "../shared_css/Random.css" 5 4 6 import {useDispatch, useSelector} from "react-redux"; 5 7 import {useEffect, useState} from "react"; … … 12 14 import {Link} from "react-router-dom"; 13 15 import JobType from "../../enumerations/JobType"; 16 import {RecruiterActions} from "../../redux/actions/recruiterActions"; 14 17 15 export const JobAdvertisements = (props) => { 18 19 export const Workspace = (props) => { 16 20 17 21 const dispatch = useDispatch(); 22 const [dispatched, setDispatched] = useState(false) 23 24 const auth = useSelector(state => (state.auth.currentUser)) 25 18 26 const [jobAdvertisementsByRecruiter, setJobAdvertisementsByRecruiter] = useState([]); 19 const auth = useSelector(state => (state.auth.currentUser))20 27 let jobAdvertisementsByRecruiterState = useSelector(state => (state.jobAd.jobAdvertisementsByRecruiter)) 21 28 22 const [role, setRole] = useState(""); 29 const [recruiterDetails, setRecruiterDetails] = useState(null); 30 23 31 const [selectedSortOrder, setSelectedSortOrder] = useState("date_newest"); 24 32 const [selectedIndustry, setSelectedIndustry] = useState("all"); 25 33 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]); 27 46 28 47 29 48 useEffect(() => { 30 if (auth) {31 setRole(auth.role);32 }33 }, [auth]);34 35 useEffect(() => {36 49 if (!dispatched && jobAdvertisementsByRecruiterState.length === 0) { 37 dispatch(JobAdvertisementActions.fetchJobAdvertisementsByRecruiter( (success, response) => {50 dispatch(JobAdvertisementActions.fetchJobAdvertisementsByRecruiter(auth.id, (success, response) => { 38 51 if (success && response.data.length > 0) { 39 52 setJobAdvertisementsByRecruiter(sortElementsBy(response.data)) … … 46 59 setJobAdvertisementsByRecruiter(jobAdvertisementsByRecruiterState) 47 60 console.log("Fetch job advertisements by recruiter STATE") 61 62 setActiveJobListingsCount(countActiveJobListings(jobAdvertisementsByRecruiterState)); 48 63 } 64 49 65 }, [jobAdvertisementsByRecruiterState]) 50 66 51 67 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); 61 73 } 62 )74 }) 63 75 } 64 76 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"> 68 90 <div className="row"> 69 <div className="col-md-12 filter- container">91 <div className="col-md-12 filter-box"> 70 92 <div className="search-container"> 71 93 <i className="fa-solid fa-magnifying-glass search-icon"></i> … … 96 118 /> 97 119 </div> 98 <button onClick={filterJobAdvertisements} className="b tn-open-modal">Find jobs</button>120 <button onClick={filterJobAdvertisements} className="blue-submit-button">Find jobs</button> 99 121 </div> 100 122 </div> 101 123 </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"> 103 125 <AddJobAdModal/> 104 126 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}/> 118 138 </div> 119 <div className="card-body">120 <h5 className="card-title">{jobAd.title}</h5>121 <span>{jobAd.industry} • <span style={{122 color: "black",123 124 125 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"> 126 146 <span><i className="fa-solid fa-building" 127 147 style={{color: "#000000"}}></i> Company: <span style={{ 128 color: "black", 129 fontWeight: "bold" 148 color: "black", fontWeight: "bold" 130 149 }}>{jobAd.recruiterName}</span></span> <br/> 131 150 </div> 132 151 133 134 <Link to={`/my-job-advertisements/${jobAd.id}/applications`}135 136 152 <div className="aligned"> 153 <Link to={`/job-management-hub/applications/${jobAd.id}`} 154 className="card-button solo">View applications</Link> 155 </div> 137 156 138 </div>139 157 </div> 140 158 </div> 141 ))} 142 159 </div>))} 143 160 </div> 144 161 </div> 162 163 145 164 ) 146 165 } -
jobvista-frontend/src/views/shared_css/Modal.css
rb248810 rbefb988 2 2 .modal-wrap { 3 3 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;*/15 4 } 16 5 … … 26 15 background: #f1f1f1; 27 16 border-radius: 10px; 28 width: 1300px !important;29 //min-width: 300px;17 min-width: 1000px !important; 18 max-width: 1000px !important; 30 19 height: auto; 31 20 padding: 0 !important; … … 41 30 border-top-right-radius: 10px; 42 31 background-color: rgba(1,38,90,0.9); 32 /*background-color: white;*/ 43 33 background-size: cover; 44 34 padding: 14px 48px; 35 /*color: #3A3B3C;*/ 45 36 color: white; 37 border-bottom: 1px solid #E5E4E7; 38 display: flex; 39 justify-content: left; 46 40 } 47 41 … … 50 44 font-weight: bold; 51 45 display: inline; 46 margin: 0; 52 47 } 53 48 … … 60 55 } 61 56 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; 64 62 } 65 63 … … 69 67 } 70 68 71 input {69 .react-responsive-modal-modal .modal-content form input { 72 70 display: block; 73 71 padding: 5px 10px; … … 77 75 } 78 76 79 textarea {77 .react-responsive-modal-modal .modal-content form textarea { 80 78 width: 100%; 81 79 resize: none; … … 89 87 } 90 88 91 .applic taion-textarea {89 .application-textarea { 92 90 height: 100px; 93 91 } … … 108 106 } 109 107 110 . submit-btn{108 .modal-buttons button, .modal-buttons div { 111 109 position: relative; 112 110 padding: 5px 20px; … … 114 112 border: none; 115 113 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; 121 116 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; 122 137 } 123 138 … … 156 171 157 172 158 . cancel-btn,.delete-btn {173 .modal-delete-buttons .cancel-btn, .modal-delete-buttons .delete-btn { 159 174 display: inline; 160 175 border: none; … … 163 178 } 164 179 165 . delete-btn {180 .modal-delete-buttons .delete-btn { 166 181 background-color: red; 167 182 color: white; -
jobvista-frontend/src/views/static/Header.css
rb248810 rbefb988 6 6 background-size: contain; 7 7 margin-right: 0; 8 8 9 } 9 10 … … 18 19 19 20 .navbar { 21 width: calc(100% - 17px); 20 22 height: 80px; 21 background-color: # 535C91;23 background-color: #f8f9fa; 22 24 font-family: Poppins, sans-serif; 25 position: fixed; 26 z-index: 10; 27 box-sizing: border-box; 23 28 } 24 29 25 30 .navbar .nav-item { 26 27 31 color: rgba(1,38,90,0.9); 28 32 margin-right: 15px; … … 31 35 font-size: 20px; 32 36 font-weight: 600; 33 //font-family: 'Ubuntu', sans-serif;34 37 font-family: Poppins, sans-serif; 35 38 } … … 37 40 color: white; 38 41 background-color: rgba(1,38,90,0.9); 39 /*border-bottom: 3px solid rgba(1,38,90,0.9);*/40 42 } 41 43 .active { … … 44 46 } 45 47 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; 59 92 color: black; 60 93 padding-top: 10px; 61 margin-bottom: 0; 62 63 } 64 65 .role { 94 95 } 96 97 .navigation .user-box .role { 66 98 color: darkgray; 67 99 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 1 1 import {Link, NavLink} from "react-router-dom"; 2 2 import "./Header.css" 3 import { jwtDecode} from "jwt-decode";3 import {jwtDecode} from "jwt-decode"; 4 4 import {useDispatch, useSelector} from 'react-redux'; 5 5 import {useEffect, useState} from "react"; … … 8 8 import {useNavigate} from "react-router"; 9 9 import {AUTH_TOKEN} from "../../axios/axiosInstance"; 10 import {JobSeekerActions} from "../../redux/actions/JobSeekerActions"; 11 import {RecruiterActions} from "../../redux/actions/recruiterActions"; 10 12 11 13 export const Header = (props) => { … … 17 19 const [role, setRole] = useState(""); 18 20 const [username, setUsername] = useState(""); 21 const [user, setUser] = useState(""); 19 22 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); 21 30 22 31 const signOut = () => { 23 32 dispatch(AuthActions.signOut()); 24 33 window.location = "/"; 34 //navigator("/") 25 35 } 36 const [isActive, setIsActive] = useState(false); 37 38 const toggleMenu = () => { 39 setIsActive(!isActive); 40 }; 26 41 27 42 useEffect(() => { 28 43 const token = localStorage.getItem(AUTH_TOKEN); 29 if (token !=null) {44 if (token != null) { 30 45 try { 31 46 const decodedToken = jwtDecode(token); … … 34 49 role: decodedToken.role, 35 50 hasAccess: decodedToken.hasAccess, 51 id: decodedToken.id 36 52 }); 37 53 } catch (error) { … … 45 61 setRole(auth.role); 46 62 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 } 47 81 } 48 82 }, [auth]); … … 51 85 <nav className="navbar navbar-expand-lg bg-light"> 52 86 <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"> 56 90 <ul className="navbar-nav me-auto mb-2 mb-lg-0"> 57 91 <NavLink to="/" className="nav-item nav-link">Home</NavLink> 58 {role ==Roles.JOBSEEKER &&92 {role == Roles.JOBSEEKER && 59 93 <> 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> 69 95 </> 70 96 } 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 && 72 103 <> 73 104 <NavLink to="/admin-panel" className="nav-item nav-link">Admin Panel</NavLink> 74 105 </> 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> 75 126 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 } 80 144 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 } 101 155 </div> 102 156 </nav>
Note:
See TracChangeset
for help on using the changeset viewer.