Changeset 4d97b63
- Timestamp:
- 08/30/24 15:44:27 (3 months ago)
- Branches:
- main
- Parents:
- 0f0add0
- Files:
-
- 20 added
- 36 edited
Legend:
- Unmodified
- Added
- Removed
-
jobvista-backend/.gitignore
r0f0add0 r4d97b63 32 32 ### VS Code ### 33 33 .vscode/ 34 35 .env -
jobvista-backend/pom.xml
r0f0add0 r4d97b63 63 63 <scope>test</scope> 64 64 </dependency> 65 <!-- other --> 66 <dependency> 67 <groupId>org.springframework.boot</groupId> 68 <artifactId>spring-boot-starter-oauth2-client</artifactId> 69 </dependency> 65 70 66 <!-- other --> 71 <dependency> 72 <groupId>org.springframework.boot</groupId> 73 <artifactId>spring-boot-starter-mail</artifactId> 74 </dependency> 75 76 <dependency> 77 <groupId>org.springframework.security</groupId> 78 <artifactId>spring-security-oauth2-jose</artifactId> 79 </dependency> 80 81 <dependency> 82 <groupId>com.google.api-client</groupId> 83 <artifactId>google-api-client</artifactId> 84 <version>2.5.1</version> 85 </dependency> 86 87 <!-- https://mvnrepository.com/artifact/com.google.oauth-client/google-oauth-client --> 88 <dependency> 89 <groupId>com.google.oauth-client</groupId> 90 <artifactId>google-oauth-client-jetty</artifactId> 91 <version>1.34.1</version> 92 </dependency> 93 94 <dependency> 95 <groupId>com.google.http-client</groupId> 96 <artifactId>google-http-client-jackson2</artifactId> 97 <version>1.32.1</version> 98 </dependency> 99 100 101 102 103 104 67 105 <!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api --> 68 106 <dependency> … … 71 109 <version>3.0.2</version> 72 110 </dependency> 73 74 75 111 76 112 <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --> -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/JobvistaBackendApplication.java
r0f0add0 r4d97b63 27 27 admin.setEmail("admin@admin.com"); 28 28 admin.setHasAccess(true); 29 // admin.setName("admin");30 // admin.setSurname("admin");31 29 admin.setPassword(new BCryptPasswordEncoder().encode("admin")); 32 30 userRepository.save(admin); -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/config/SecurityConfiguration.java
r0f0add0 r4d97b63 33 33 .requestMatchers( 34 34 "/api/auth/**", 35 "/oauth2/**", 35 36 "/api/job-advertisements/**", 36 37 "/api/applications/**", 37 38 "/api/recruiter/**", 38 "/api/job-seeker/**" 39 "/api/job-seeker/**", 40 "/uploads/**" 39 41 ).permitAll() 40 42 .requestMatchers("/api/admin/**").hasAnyAuthority(Role.ROLE_ADMIN.name()) … … 46 48 .requestMatchers("/api/job-advertisements/edit/{id}").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 47 49 .requestMatchers("/api/job-advertisements/delete/{id}").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 48 .requestMatchers("/api/applications/{id}/update").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 50 .requestMatchers("/api/applications/{id}/update").hasAnyAuthority(Role.ROLE_JOBSEEKER.name()) 51 .requestMatchers("/api/applications/update").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 52 .requestMatchers("/uploads/applications/**").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 49 53 .requestMatchers("/api/job-advertisements/{advertisement_id}/applications").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 54 .requestMatchers("/api/job-advertisements/{advertisement_id}/applications/filtered").hasAnyAuthority(Role.ROLE_RECRUITER.name()) 50 55 .requestMatchers("/api/applications/submit").hasAnyAuthority(Role.ROLE_JOBSEEKER.name()) 51 56 .requestMatchers("/api/my-applications/{id}").hasAnyAuthority(Role.ROLE_JOBSEEKER.name()) 52 .anyRequest().authenticated()) 57 .requestMatchers("/api/my-applications/{id}/filtered").hasAnyAuthority(Role.ROLE_JOBSEEKER.name()) 58 .anyRequest().authenticated()) 53 59 .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 54 60 .authenticationProvider(authenticationProvider()).addFilterBefore( 55 jwtAuthFilter, UsernamePasswordAuthenticationFilter.class 56 ); 61 jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) 62 .oauth2Login(oauth2 -> oauth2 63 .defaultSuccessUrl("/api/auth/google", true) 64 ) 65 ; 66 57 67 return http.build(); 58 68 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/ApplicationController.java
r0f0add0 r4d97b63 30 30 } 31 31 32 @PostMapping("/my-applications/{id}/filtered") 33 public ResponseEntity<?> filterApplicationsByJobSeekerId(@PathVariable Long id, @RequestBody String status) { 34 List<ApplicationDetailsDTO> applicationList = applicationService.filterByJobSeekerId(id, status); 35 return new ResponseEntity<>(applicationList, HttpStatus.OK); 36 } 37 32 38 @GetMapping("/job-advertisements/{advertisement_id}/applications") 33 39 public ResponseEntity<?> findAllApplicationsByJobAdvertisementId(@PathVariable("advertisement_id") Long advertisementId) { … … 36 42 } 37 43 38 @PostMapping("/applications/{id}/update") 44 @PostMapping("/job-advertisements/{advertisement_id}/applications/filtered") 45 public ResponseEntity<?> filterApplicationsByJobAdvertisementId(@PathVariable("advertisement_id") Long advertisementId, @RequestBody String status) { 46 List<ApplicationDetailsDTO> applicationList = applicationService.filterByJobAdvertisementId(advertisementId, status); 47 return new ResponseEntity<>(applicationList, HttpStatus.OK); 48 } 49 50 @PostMapping("/applications/{id}/update/NOT-IN-USE") 39 51 public ResponseEntity<?> updateApplicationStatus(@PathVariable("id") Long applicaitonId, @RequestBody ApplicationStatusDTO appStatusDTO) { 40 52 ApplicationStatusDTO applicationStatusDTO = applicationService.updateApplicationStatus(applicaitonId,appStatusDTO.getStatus()); 41 53 return new ResponseEntity<>(applicationStatusDTO, HttpStatus.OK); 54 } 55 56 @PostMapping("/applications/update") 57 public ResponseEntity<?> updateApplications(@RequestBody List<ApplicationStatusDTO> changes) { 58 List<ApplicationStatusDTO> updatedApplications = applicationService.updateApplications(changes); 59 return new ResponseEntity<>(updatedApplications, HttpStatus.OK); 42 60 } 43 61 … … 66 84 return new ResponseEntity<>(applicationDetailsDTO, HttpStatus.OK); 67 85 } 86 87 @PostMapping("/applications/{id}/update") 88 public ResponseEntity<ApplicationDetailsDTO> updateApplication( 89 @PathVariable("id") Long applicationId, 90 @RequestParam("additionalFiles") MultipartFile[] additionalFiles) { 91 ApplicationDetailsDTO applicationDetailsDTO = applicationService.updateApplication(applicationId, additionalFiles); 92 return new ResponseEntity<>(applicationDetailsDTO, HttpStatus.OK); 93 } 94 95 @GetMapping("/applications/{id}/download-additional-files") 96 public ResponseEntity<List<String>> getAdditionalFilesUrls(@PathVariable("id") Long applicationId) { 97 List<String> fileUrls = applicationService.loadAdditionalFilesAsUrls(applicationId); 98 return ResponseEntity.ok(fileUrls); 99 } 100 68 101 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/AuthController.java
r0f0add0 r4d97b63 7 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.JobSeekerMapper; 8 8 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.RecruiterMapper; 9 9 10 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AuthService; 11 10 12 import org.springframework.http.HttpStatus; 11 13 import org.springframework.http.ResponseEntity; 12 14 import org.springframework.web.bind.annotation.*; 15 16 import java.util.Map; 17 18 13 19 14 20 @RestController … … 42 48 return ResponseEntity.ok(authenticationService.refreshToken(refreshTokenRequest)); 43 49 } 50 51 @PostMapping("/google") 52 public ResponseEntity<?> googleSignIn(@RequestBody Map<String, String> token) { 53 return ResponseEntity.ok(authenticationService.googleSignIn(token)); 54 } 44 55 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/Application.java
r0f0add0 r4d97b63 9 9 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.job_advertisements.JobAdvertisement; 10 10 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker; 11 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User;12 11 13 12 import java.time.LocalDateTime; 14 import java.util. HashMap;13 import java.util.ArrayList; 15 14 import java.util.List; 16 15 … … 45 44 private ApplicationStatus status; 46 45 46 private String response; 47 48 @ElementCollection 49 private List<String> additionalFilePaths; 50 47 51 public Application(JobSeeker jobSeeker, JobAdvertisement jobAdvertisement, List<String> answers, String message) { 48 52 this.jobSeeker = jobSeeker; … … 53 57 submittedOn = LocalDateTime.now(); 54 58 this.status = ApplicationStatus.PROPOSED; 59 this.response = ""; 60 this.additionalFilePaths = new ArrayList<>(); 55 61 } 56 62 … … 72 78 application.getMessage(), 73 79 application.getSubmittedOn(), 74 application.getStatus().name() 80 application.getStatus().name(), 81 application.getResponse(), 82 application.getAdditionalFilePaths() 75 83 ); 76 84 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationDetailsDTO.java
r0f0add0 r4d97b63 29 29 private LocalDateTime submittedOn; 30 30 private String status; 31 private String response; 32 private List<String> additionalFileNames; 31 33 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationStatusDTO.java
r0f0add0 r4d97b63 9 9 Long id; 10 10 String status; 11 String response; 11 12 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/repositories/JobSeekerRepository.java
r0f0add0 r4d97b63 4 4 import org.springframework.data.jpa.repository.JpaRepository; 5 5 6 import java.util.Optional; 7 6 8 public interface JobSeekerRepository extends JpaRepository<JobSeeker, Long> { 9 Optional<JobSeeker> findByEmail(String email); 7 10 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/ApplicationServiceImpl.java
r0f0add0 r4d97b63 6 6 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker; 7 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.JobSeekerRepository; 8 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.EmailSenderService; 8 9 import org.springframework.core.io.Resource; 9 10 import org.springframework.core.io.UrlResource; … … 19 20 import org.springframework.beans.factory.annotation.Value; 20 21 import org.springframework.stereotype.Service; 22 import org.springframework.web.multipart.MultipartFile; 23 import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 21 24 22 25 import java.io.IOException; 26 import java.net.MalformedURLException; 23 27 import java.nio.file.Files; 24 28 import java.nio.file.Path; … … 39 43 40 44 @Autowired 45 private EmailSenderService emailSenderService; 46 47 @Autowired 41 48 public ApplicationServiceImpl(@Value("${file.upload-dir}") String uploadDir, UserRepository userRepository, ApplicationRepository applicationRepository, JobAdvertisementRepository jobAdvertisementRepository, 42 49 JobSeekerRepository jobSeekerRepository) { … … 95 102 96 103 @Override 104 public ApplicationDetailsDTO updateApplication(Long applicationId, MultipartFile[] additionalFiles) { 105 Application application = applicationRepository.findById(applicationId).orElse(null); 106 if(application== null) { 107 throw new RuntimeException("Application not found."); 108 } 109 110 for (MultipartFile additionalFile : additionalFiles) { 111 if (additionalFile.isEmpty()) { 112 throw new RuntimeException("Failed to store empty file."); 113 } 114 } 115 116 Path filesPath = this.fileStorageLocation.resolve(String.valueOf(application.getId())).resolve("additional_files"); 117 for(MultipartFile additionalFile: additionalFiles) { 118 Path targetLocation = filesPath.resolve(additionalFile.getOriginalFilename()); 119 120 try { 121 Files.createDirectories(filesPath); 122 Files.copy(additionalFile.getInputStream(), targetLocation); 123 } catch (IOException e) { 124 throw new RuntimeException(e); 125 } 126 127 String relativePath = Paths.get("uploads","applications",String.valueOf(application.getId()), 128 "additional_files", additionalFile.getOriginalFilename()).toString(); 129 List<String> currentAdditionalFilePaths = application.getAdditionalFilePaths(); 130 currentAdditionalFilePaths.add(relativePath); 131 application.setAdditionalFilePaths(currentAdditionalFilePaths); 132 application = applicationRepository.save(application); 133 } 134 return Application.mapToApplicationDetailsDTO(application); 135 } 136 137 @Override 97 138 public List<ApplicationDetailsDTO> findAllByJobAdvertisementId(Long jobId) { 98 139 List<Application> applications = applicationRepository.findAllByJobAdvertisementId(jobId); … … 101 142 102 143 @Override 144 public List<ApplicationDetailsDTO> filterByJobAdvertisementId(Long jobId, String status) { 145 List<Application> applications = applicationRepository.findAllByJobAdvertisementId(jobId); 146 String statusTrimmed = status.subSequence(0, status.length()-1).toString(); 147 148 if(statusTrimmed.equals("ALL")) { 149 applications = applicationRepository.findAllByJobAdvertisementId(jobId); 150 } else { 151 applications = applications.stream().filter(application -> application.getStatus().name().equals(statusTrimmed)).toList(); 152 } 153 return applications.stream().map(Application::mapToApplicationDetailsDTO).toList(); 154 } 155 156 @Override 103 157 public List<ApplicationDetailsDTO> findAllByJobSeekerId(Long jobSeekerId) { 104 158 List<Application> applications = applicationRepository.findAllByJobSeekerId(jobSeekerId); 105 159 return applications.stream().map(Application::mapToApplicationDetailsDTO).toList(); 160 } 161 162 @Override 163 public List<ApplicationDetailsDTO> filterByJobSeekerId(Long jobSeekerId, String status) { 164 List<Application> applications = applicationRepository.findAllByJobSeekerId(jobSeekerId); 165 String statusTrimmed = status.subSequence(0, status.length()-1).toString(); 166 if(statusTrimmed.equals("ALL")) { 167 applications = applicationRepository.findAllByJobSeekerId(jobSeekerId); 168 } else { 169 applications = applications.stream().filter(application -> application.getStatus().name().equals(statusTrimmed)).toList(); 170 } 171 return applications.stream().map(Application::mapToApplicationDetailsDTO).toList(); 106 172 } 107 173 … … 126 192 } 127 193 194 public List<String> loadAdditionalFilesAsUrls(Long applicationId) { 195 Application application = applicationRepository.findById(applicationId) 196 .orElseThrow(() -> new IllegalArgumentException("Application not found")); 197 198 List<String> fileUrls = new ArrayList<>(); 199 List<String> relativeFilePaths = application.getAdditionalFilePaths(); 200 201 for (String relativeFilePath : relativeFilePaths) { 202 //TO DO: refactor 203 Path filePath = Paths.get(fileStorageLocation.getParent().getParent().toString(), relativeFilePath).normalize(); 204 String relativePath = filePath.toString().replace("\\", "/").replaceFirst("^.+uploads", "uploads"); 205 206 String fileUrl = ServletUriComponentsBuilder.fromCurrentContextPath() 207 .path("/") 208 .path(relativePath) 209 .toUriString(); 210 fileUrls.add(fileUrl); 211 } 212 213 return fileUrls; 214 } 215 216 /* @Override 217 public List<Resource> loadAdditionalFilesAsZippedResource(Long applicationId) { 218 Application application = applicationRepository.findById(applicationId). 219 orElseThrow(() -> new IllegalArgumentException("Application not found")); 220 221 List<Resource> resources = new ArrayList<>(); 222 223 List<String> relativeFilePaths = application.getAdditionalFilePaths(); 224 for(String relativeFilePath: relativeFilePaths) { 225 Path filePath = fileStorageLocation.getParent().getParent().resolve(relativeFilePath).normalize(); 226 227 try { 228 Resource resource = new UrlResource(filePath.toUri()); 229 if (resource.exists()) { 230 resources.add(resource); 231 } 232 } catch (MalformedURLException e) { 233 throw new RuntimeException(e); 234 } 235 } 236 return resources; 237 }*/ 238 239 @Override 240 public List<ApplicationStatusDTO> updateApplications(List<ApplicationStatusDTO> updates) { 241 List<ApplicationStatusDTO> updatedApplications = new ArrayList<>(); 242 243 for(ApplicationStatusDTO applicationStatusDTO : updates) { 244 Application application = applicationRepository.findById(applicationStatusDTO.getId()).orElse(null); 245 if(application != null) { 246 application.setStatus(ApplicationStatus.valueOf(applicationStatusDTO.getStatus())); 247 application.setResponse(applicationStatusDTO.getResponse()); 248 applicationRepository.save(application); 249 updatedApplications.add(applicationStatusDTO); 250 251 //email notification 252 String email = application.getJobSeeker().getEmail(); 253 String subject = application.getJobAdvertisement().getRecruiter().getName() + ": " + application.getJobAdvertisement().getTitle() + " - STATUS UPDATE"; 254 String text = "Dear " + application.getJobSeeker().getName() + ",\n\n"; 255 256 switch (applicationStatusDTO.getStatus()) { 257 case "ACCEPTED": 258 text += "Great news! Your application has been accepted.\n\n"; 259 break; 260 case "DENIED": 261 text += "We regret to inform you that your application has been denied. We appreciate your interest and effort.\n\n"; 262 break; 263 case "PROPOSED": 264 text += "Your application status has been updated to 'Proposed'. We're considering your application for the next phase.\n\n"; 265 break; 266 case "UNDER_REVIEW": 267 text += "Your application is currently under review.\n\n"; 268 break; 269 } 270 271 272 if(!applicationStatusDTO.getResponse().isEmpty()) { 273 text += "Response: " + applicationStatusDTO.getResponse() + "\n\n"; 274 } 275 text += "Thank you."; 276 emailSenderService.sendEmail(email, subject, text); 277 } 278 } 279 return updatedApplications; 280 } 281 128 282 @Override 129 283 public ApplicationStatusDTO updateApplicationStatus(Long id, String status) { … … 132 286 application.setStatus(ApplicationStatus.valueOf(status)); 133 287 applicationRepository.save(application); 134 return new ApplicationStatusDTO(id, status );288 return new ApplicationStatusDTO(id, status, ""); 135 289 } 136 290 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/AuthServiceImpl.java
r0f0add0 r4d97b63 1 1 package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.impl; 2 2 3 import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; 4 import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; 5 import com.google.api.client.http.javanet.NetHttpTransport; 6 import com.google.api.client.json.jackson2.JacksonFactory; 3 7 import lombok.RequiredArgsConstructor; 8 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.config.GoogleOAuth2Properties; 9 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.controllers.AuthController; 10 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role; 4 11 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.SignInDTO; 5 12 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JwtAuthResponse; … … 12 19 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.UserRepository; 13 20 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AuthService; 21 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JobSeekerService; 14 22 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JwtService; 15 23 import org.springframework.security.authentication.AuthenticationManager; 16 24 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 25 import org.springframework.security.core.authority.SimpleGrantedAuthority; 17 26 import org.springframework.security.crypto.password.PasswordEncoder; 27 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 28 import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; 29 import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; 30 import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 31 import org.springframework.security.oauth2.core.user.OAuth2User; 18 32 import org.springframework.stereotype.Service; 19 33 import org.springframework.web.multipart.MultipartFile; 34 35 import javax.imageio.ImageIO; 36 import java.awt.image.BufferedImage; 37 import java.io.ByteArrayInputStream; 38 import java.io.ByteArrayOutputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.net.URL; 20 42 import java.time.LocalDateTime; 43 import java.util.Collections; 21 44 import java.util.HashMap; 45 import java.util.Map; 46 import java.util.Optional; 22 47 23 48 @Service … … 30 55 private final AuthenticationManager authenticationManager; 31 56 private final UserRepository userRepository; 57 private final JobSeekerService jobSeekerService; 32 58 private final JwtService jwtService; 59 private final GoogleOAuth2Properties googleOAuth2Properties; 33 60 34 61 @Override … … 56 83 return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), user.isHasAccess(), jwt, refreshJwt); 57 84 } 58 85 59 86 public JwtAuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest) { 60 87 String userEmail = jwtService.extractUsername(refreshTokenRequest.getToken()); 61 88 User user = userRepository.findByEmail(userEmail).orElseThrow(); 62 if (jwtService.isTokenValid(refreshTokenRequest.getToken(), user)) {89 if (jwtService.isTokenValid(refreshTokenRequest.getToken(), user)) { 63 90 String jwt = jwtService.generateToken(user); 64 91 … … 67 94 return null; 68 95 } 96 97 @Override 98 public JwtAuthResponse googleSignIn(Map<String, String> token) { 99 OAuth2AuthenticationToken authentication = getAuthentication(token.get("tokenId")); 100 101 OAuth2User oAuth2User = authentication.getPrincipal(); 102 String email = oAuth2User.getAttribute("email"); 103 104 JobSeeker jobSeeker = jobSeekerRepository.findByEmail(email) 105 .orElseGet(() -> { 106 JobSeeker newJobSeeker = new JobSeeker(); 107 newJobSeeker.setEmail(email); 108 newJobSeeker.setFirstName(oAuth2User.getAttribute("given_name")); 109 newJobSeeker.setLastName(oAuth2User.getAttribute("family_name")); 110 newJobSeeker.setPassword(""); 111 newJobSeeker.setRole(Role.ROLE_JOBSEEKER); 112 newJobSeeker.setHasAccess(true); 113 jobSeekerRepository.save(newJobSeeker); 114 115 String googleProfilePicUrl = oAuth2User.getAttribute("picture"); 116 submitGoogleProfilePic(newJobSeeker.getId(), googleProfilePicUrl); 117 118 return newJobSeeker; 119 }); 120 121 String jwt = jwtService.generateToken(jobSeeker); 122 123 return new JwtAuthResponse( 124 jobSeeker.getId(), 125 jobSeeker.getEmail(), 126 jobSeeker.getFirstName() + " " + jobSeeker.getLastName(), 127 jobSeeker.getRole().name(), 128 jobSeeker.isHasAccess(), 129 jwt, 130 null 131 ); 132 } 133 134 public OAuth2AuthenticationToken getAuthentication(String tokenId) { 135 try { 136 GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory()) 137 .setAudience(Collections.singletonList(googleOAuth2Properties.getClientId())) 138 .build(); 139 140 GoogleIdToken idToken = verifier.verify(tokenId); 141 if (idToken != null) { 142 GoogleIdToken.Payload payload = idToken.getPayload(); 143 144 String userId = payload.getSubject(); 145 String email = payload.getEmail(); 146 boolean emailVerified = Boolean.TRUE.equals(payload.getEmailVerified()); 147 String name = (String) payload.get("name"); 148 String pictureUrl = (String) payload.get("picture"); 149 String familyName = Optional.ofNullable((String) payload.get("family_name")).orElse(""); 150 String givenName = (String) payload.get("given_name"); 151 152 Map<String, Object> attributes = Map.of( 153 "sub", userId, 154 "email", email, 155 "email_verified", emailVerified, 156 "name", name, 157 "picture", pictureUrl, 158 "family_name", familyName, 159 "given_name", givenName 160 ); 161 162 OAuth2User oAuth2User = new DefaultOAuth2User( 163 Collections.singleton(new SimpleGrantedAuthority("ROLE_JOBSEEKER")), 164 attributes, 165 "sub" 166 ); 167 168 return new OAuth2AuthenticationToken(oAuth2User, oAuth2User.getAuthorities(), "google"); 169 } else { 170 throw new IllegalArgumentException("Invalid ID token"); 171 } 172 } catch (Exception e) { 173 throw new RuntimeException("Failed to verify token", e); 174 } 175 } 176 177 public void submitGoogleProfilePic(Long jobSeekerId, String googleProfilePicUrl) { 178 try { 179 URL url = new URL(googleProfilePicUrl); 180 BufferedImage image = ImageIO.read(url); 181 182 // Convert BufferedImage to byte array 183 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 184 ImageIO.write(image, "jpg", baos); 185 byte[] imageBytes = baos.toByteArray(); 186 187 // Convert byte array to MultipartFile 188 MultipartFile multipartFile = new InMemoryMultipartFile("profilePicFile", "google-profile-pic.jpg", "image/jpeg", imageBytes); 189 190 jobSeekerService.submitProfilePic(jobSeekerId, multipartFile); 191 } catch (IOException e) { 192 e.printStackTrace(); 193 } 194 } 195 196 class InMemoryMultipartFile implements MultipartFile { 197 198 private final String name; 199 private final String originalFilename; 200 private final String contentType; 201 private final byte[] content; 202 203 public InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] content) { 204 this.name = name; 205 this.originalFilename = originalFilename; 206 this.contentType = contentType; 207 this.content = content; 208 } 209 210 @Override 211 public String getName() { 212 return name; 213 } 214 215 @Override 216 public String getOriginalFilename() { 217 return originalFilename; 218 } 219 220 @Override 221 public String getContentType() { 222 return contentType; 223 } 224 225 @Override 226 public boolean isEmpty() { 227 return content.length == 0; 228 } 229 230 @Override 231 public long getSize() { 232 return content.length; 233 } 234 235 @Override 236 public byte[] getBytes() throws IOException { 237 return content; 238 } 239 240 @Override 241 public InputStream getInputStream() throws IOException { 242 return new ByteArrayInputStream(content); 243 } 244 245 @Override 246 public void transferTo(java.io.File dest) throws IOException { 247 throw new UnsupportedOperationException("This method is not implemented"); 248 } 249 } 69 250 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/ApplicationService.java
r0f0add0 r4d97b63 7 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.applications.DTO.ApplicationStatusDTO; 8 8 import org.springframework.core.io.Resource; 9 import org.springframework.web.multipart.MultipartFile; 9 10 10 11 import java.util.List; … … 12 13 public interface ApplicationService { 13 14 ApplicationDetailsDTO submitApplication(ApplicationDTO applicationDTO); 15 ApplicationDetailsDTO updateApplication(Long applicationId, MultipartFile[] additionalFiles); 14 16 List<ApplicationDetailsDTO> findAllByJobAdvertisementId(Long jobId); 17 List<ApplicationDetailsDTO> filterByJobAdvertisementId(Long jobId, String status); 15 18 List<ApplicationDetailsDTO> findAllByJobSeekerId(Long jobSeekerId); 19 List<ApplicationDetailsDTO> filterByJobSeekerId(Long jobSeekerId, String status); 16 20 Resource loadResumeAsResource(Long applicationId); 21 List<String> loadAdditionalFilesAsUrls(Long applicationId); 17 22 ApplicationStatusDTO updateApplicationStatus(Long id, String status); 23 List<ApplicationStatusDTO> updateApplications(List<ApplicationStatusDTO> updates); 18 24 } -
jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/AuthService.java
r0f0add0 r4d97b63 7 7 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter; 8 8 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User; 9 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 10 11 import java.util.Map; 9 12 10 13 public interface AuthService { … … 13 16 JwtAuthResponse signIn(SignInDTO signInDTO); 14 17 JwtAuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest); 18 19 JwtAuthResponse googleSignIn(Map<String, String> token); 20 OAuth2AuthenticationToken getAuthentication(String tokenId); 21 void submitGoogleProfilePic(Long jobSeekerId, String googleProfilePicUrl); 15 22 } -
jobvista-backend/src/main/resources/application.properties
r0f0add0 r4d97b63 2 2 3 3 spring.datasource.driver-class-name=org.postgresql.Driver 4 spring.datasource.url= jdbc:postgresql://localhost:5432/jobvistaDB5 spring.datasource.username= postgres6 spring.datasource.password= postgres4 spring.datasource.url=${db_url} 5 spring.datasource.username=${db_username} 6 spring.datasource.password=${db_password} 7 7 8 8 #spring.jpa.hibernate.ddl-auto=create-drop … … 12 12 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect 13 13 14 file.upload-dir= ./uploads14 file.upload-dir=${file_upload_dir} 15 15 16 16 spring.servlet.multipart.enabled=true 17 17 spring.servlet.multipart.max-file-size=2MB 18 18 spring.servlet.multipart.max-request-size=2MB 19 20 spring.security.oauth2.client.registration.google.client-id=${google_id} 21 spring.security.oauth2.client.registration.google.client-secret=${google_secret} 22 spring.security.oauth2.client.registration.google.scope=profile, email 23 spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:3000/login/oauth2/code/google 24 spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth 25 spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token 26 spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo 27 spring.security.oauth2.client.provider.google.user-name-attribute=sub 28 29 spring.mail.host=smtp.gmail.com 30 spring.mail.port=587 31 spring.mail.username=${mail_username} 32 spring.mail.password=${mail_password} 33 custom.mail.sender.email=${mail_sender_email} 34 custom.mail.sender.name=${mail_sender_name} 35 spring.mail.properties.mail.smtp.auth=true 36 spring.mail.properties.mail.smtp.starttls.enable=true 37 38 -
jobvista-frontend/.gitignore
r0f0add0 r4d97b63 22 22 yarn-debug.log* 23 23 yarn-error.log* 24 25 .env -
jobvista-frontend/package-lock.json
r0f0add0 r4d97b63 13 13 "@hookform/resolvers": "^3.3.4", 14 14 "@mui/material": "^5.15.17", 15 "@react-oauth/google": "^0.12.1", 15 16 "@reduxjs/toolkit": "^2.2.3", 16 17 "@testing-library/jest-dom": "^5.17.0", … … 3789 3790 } 3790 3791 }, 3792 "node_modules/@react-oauth/google": { 3793 "version": "0.12.1", 3794 "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", 3795 "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", 3796 "peerDependencies": { 3797 "react": ">=16.8.0", 3798 "react-dom": ">=16.8.0" 3799 } 3800 }, 3791 3801 "node_modules/@reduxjs/toolkit": { 3792 3802 "version": "2.2.3", -
jobvista-frontend/package.json
r0f0add0 r4d97b63 8 8 "@hookform/resolvers": "^3.3.4", 9 9 "@mui/material": "^5.15.17", 10 "@react-oauth/google": "^0.12.1", 10 11 "@reduxjs/toolkit": "^2.2.3", 11 12 "@testing-library/jest-dom": "^5.17.0", -
jobvista-frontend/public/index.html
r0f0add0 r4d97b63 32 32 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" /> 33 33 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" /> 34 35 <!-- GOOGLE--> 36 <script src="https://accounts.google.com/gsi/client" async defer></script> 34 37 <title>Job Vista</title> 35 38 </head> -
jobvista-frontend/src/App.css
r0f0add0 r4d97b63 38 38 39 39 .form-container { 40 font-family: "Segoe UI"; 40 41 background-color: white; 41 42 border-radius: 10px; 42 43 padding: 15px 30px; 43 margin-top: 80px; 44 margin-top: 40px; 45 } 46 47 48 .form-container h5, .form-container h3 { 49 text-align:center; 50 font-family: Poppins 44 51 } 45 52 … … 217 224 } 218 225 219 220 221 222 226 .card-company-logo { 227 border-radius: 15% 228 } 229 230 231 232 233 -
jobvista-frontend/src/redux/actionTypes.js
r0f0add0 r4d97b63 13 13 export const FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER = "FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER" 14 14 export const SUBMIT_APPLICATION = "SUBMIT_APPLICATION" 15 export const UPDATE_APPLICATION = "UPDATE_APPLICATION" 15 16 export const UPDATE_APPLICATION_STATUS = "UPDATE_APPLICATION_STATUS" 17 export const UPDATE_APPLICATIONS = "UPDATE_APPLICATIONS" 16 18 export const FETCH_APPLICATIONS_BY_JOB_ID = "FETCH_APPLICATIONS_BY_JOB_ID" 19 export const FILTER_APPLICATIONS_BY_JOB_ID = "FILTER_APPLICATIONS_BY_JOB_ID" 17 20 export const FETCH_APPLICATIONS_BY_JOB_SEEKER_ID = "FETCH_APPLICATIONS_BY_JOB_SEEKER_ID" 21 export const FILTER_APPLICATIONS_BY_JOB_SEEKER_ID = "FILTER_APPLICATIONS_BY_JOB_SEEKER_ID" 18 22 export const DOWNLOAD_RESUME = "DOWNLOAD_RESUME" 19 23 -
jobvista-frontend/src/redux/actions/applicationActions.js
r0f0add0 r4d97b63 3 3 CURRENT_USER, 4 4 FETCH_APPLICATIONS_BY_JOB_ID, 5 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 6 SUBMIT_APPLICATION, UPDATE_APPLICATION _STATUS5 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 6 SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS, UPDATE_APPLICATIONS 7 7 } from "../actionTypes"; 8 8 … … 18 18 dispatch({ 19 19 type: SUBMIT_APPLICATION, 20 application: response.data 21 }) 22 callback(true, response) 23 }).catch(error => { 24 callback(false, error) 25 console.log(error) 26 }) 27 } 28 }, 29 30 updateApplication: (applicationId, additionalFiles, callback) => { 31 console.log(additionalFiles) 32 return dispatch => { 33 axios.post("/applications/"+ applicationId + "/update", additionalFiles, { 34 headers: { 35 'Content-Type': 'multipart/form-data' 36 } 37 }) 38 .then(response => { 39 dispatch({ 40 type: UPDATE_APPLICATION, 20 41 application: response.data 21 42 }) … … 45 66 } 46 67 }, 68 69 updateApplications: (changes, callback) => { 70 return dispatch => { 71 axios.post("/applications/update", changes) 72 .then(response => { 73 dispatch({ 74 type: UPDATE_APPLICATIONS, 75 applications: response.data 76 }) 77 callback(true, response) 78 }).catch(error => { 79 callback(false, error) 80 }) 81 } 82 }, 83 47 84 fetchApplicationsByJobSeeker: (jobSeekerId, callback) => { 48 85 return dispatch => { … … 51 88 dispatch({ 52 89 type: FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 90 applicationsByJobSeeker: response.data 91 }) 92 callback(true, response) 93 }).catch(error => { 94 callback(false, error) 95 }) 96 } 97 }, 98 99 filterApplicationsByJobSeeker: (jobSeekerId, status, callback) => { 100 return dispatch => { 101 axios.post("/my-applications/" + jobSeekerId + "/filtered", status) 102 .then(response => { 103 dispatch({ 104 type: FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 53 105 applicationsByJobSeeker: response.data 54 106 }) … … 75 127 } 76 128 }, 129 130 filterApplicationsByJobAdId: (jobAdId, status, callback) => { 131 return dispatch => { 132 axios.post("/job-advertisements/" + jobAdId + "/applications/filtered", status) 133 .then(response => { 134 dispatch({ 135 type: FILTER_APPLICATIONS_BY_JOB_ID, 136 applicationsByJobAdId: response.data 137 }) 138 callback(true, response) 139 } 140 ).catch(error => { 141 callback(false, error) 142 }) 143 } 144 }, 77 145 downloadResume: (id, callback) => { 78 146 return axios.get("/applications/" + id + "/download-resume", {responseType: "blob"}) … … 86 154 }) 87 155 156 }, 157 downloadAdditionalFiles: (id, callback) => { 158 return axios.get("/applications/" + id + "/download-additional-files") 159 .then(response => { 160 const urls = response.data; // This will be a list of URLs 161 callback(true, urls); 162 }) 163 .catch(error => { 164 callback(false, error); 165 }); 166 88 167 } 89 168 } -
jobvista-frontend/src/redux/actions/authActions.js
r0f0add0 r4d97b63 32 32 }).catch((error) => { 33 33 callback(false, error); 34 }); 35 }; 36 }, 37 38 signInGoogle: (tokenId, callback) => { 39 return dispatch => { 40 axios.post("/auth/google", { tokenId }) 41 .then(jwtResponse => { 42 const response = jwtResponse.data; 43 const token = response.token; 44 const user = { 45 name: response.name, 46 role: response.role, 47 access: response.hasAccess, 48 id: response.id, 49 }; 50 dispatch({ 51 type: SIGN_IN, 52 payload: { 53 token, 54 user 55 } 56 }); 57 callback && callback(true); 58 }).catch(error => { 59 callback && callback(false, error); 34 60 }); 35 61 }; -
jobvista-frontend/src/redux/reducers/applicationReducer.js
r0f0add0 r4d97b63 2 2 CURRENT_USER, 3 3 FETCH_APPLICATIONS_BY_JOB_ID, 4 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, 5 SUBMIT_APPLICATION, UPDATE_APPLICATION _STATUS4 FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID, 5 SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS 6 6 } from "../actionTypes"; 7 7 … … 22 22 applicationsByJobSeeker: [...state.applicationsByJobSeeker, action.application] 23 23 } 24 case UPDATE_APPLICATION: 25 return { 26 ...state, 27 applicationsByJobSeeker: state.applicationsByJobSeeker.map(application => 28 application.id === action.application.id ? 29 action.application : // Replace with the updated application 30 application // Keep the old one 31 ) 32 } 24 33 case UPDATE_APPLICATION_STATUS: 25 34 return { … … 36 45 applicationsByJobAdId: action.applicationsByJobAdId 37 46 } 47 case FILTER_APPLICATIONS_BY_JOB_ID: 48 return { 49 ...state, 50 applicationsByJobAdId: action.applicationsByJobAdId 51 } 52 38 53 case FETCH_APPLICATIONS_BY_JOB_SEEKER_ID: 54 return { 55 ...state, 56 applicationsByJobSeeker: action.applicationsByJobSeeker 57 } 58 case FILTER_APPLICATIONS_BY_JOB_SEEKER_ID: 39 59 return { 40 60 ...state, -
jobvista-frontend/src/utils/toastUtils.js
r0f0add0 r4d97b63 108 108 toast.success( 109 109 <span> 110 Status updated successfully!110 Application/s updated successfully! 111 111 </span>, { 112 112 position: "bottom-right", … … 126 126 <span> 127 127 Your application was successfully sent! 128 </span>, { 129 position: "bottom-right", 130 autoClose: 5000, 131 hideProgressBar: false, 132 closeOnClick: true, 133 pauseOnHover: false, 134 draggable: true, 135 progress: undefined, 136 theme: "dark", 137 pauseOnFocusLoss: false 138 }); 139 } 140 141 export const notifyJobAdUpdate= () => { 142 toast.success( 143 <span> 144 Your application was successfully updated! 128 145 </span>, { 129 146 position: "bottom-right", -
jobvista-frontend/src/views/applications/ApplicationDetailsModal.js
r0f0add0 r4d97b63 17 17 import Roles from "../../enumerations/Roles"; 18 18 import {ApplicationActions} from "../../redux/actions/applicationActions"; 19 import {notifyJobAdApply, notifyJobAdUpdate} from "../../utils/toastUtils"; 19 20 20 21 … … 25 26 const auth = useSelector(state => state.auth.currentUser) 26 27 const [resumeUrl, setResumeUrl] = useState(""); 28 const [additionalFileUrls, setAdditionalFileUrls] = useState([]); 27 29 28 //const [resumeFile, setResumeFile] = useState(null);30 const [additionalFiles, setAdditionalFiles] = useState(null); 29 31 const toggleModal = () => { 30 32 setModal(!modal); 31 33 }; 34 35 const {register, handleSubmit, control, formState: {errors}} = useForm(); 32 36 33 37 useEffect(() => { … … 36 40 if (success) { 37 41 setResumeUrl(response); 42 43 if (application.additionalFileNames.length > 0) { 44 ApplicationActions.downloadAdditionalFiles(application.id, (success2, response) => { 45 if (success2) { 46 setAdditionalFileUrls(response); 47 } 48 }) 49 } 38 50 } 39 51 }) 40 52 } 41 53 }, []) 54 55 const updateApplication = async () => { 56 try { 57 const formData = new FormData(); 58 if (additionalFiles && additionalFiles.length > 0) { 59 for (let i = 0; i < additionalFiles.length; i++) { 60 formData.append('additionalFiles', additionalFiles[i]); 61 } 62 } 63 64 dispatch(ApplicationActions.updateApplication(application.id, formData, (success) => { 65 if (success) { 66 toggleModal() 67 window.location.reload() 68 } 69 })) 70 } catch (err) { 71 console.error(err) 72 } 73 } 42 74 43 75 function getFileName(path) { … … 50 82 51 83 return (<div className="modal-wrap"> 52 <button onClick={toggleModal} className="application-button">View application</button> 84 {auth.role === Roles.RECRUITER ? <button onClick={toggleModal} className="application-button">View 85 application</button> : (application.status === "UNDER_REVIEW" && application.response.length > 0 && additionalFileUrls.length === 0) ? 86 <button onClick={toggleModal} className="application-button">Update application</button> : 87 <button onClick={toggleModal} className="application-button">View application</button>} 88 53 89 <Modal open={modal} onClose={toggleModal} center> 54 90 <div className="head-modal"> … … 58 94 59 95 <div className="modal-content"> 60 <form >96 <form onSubmit={handleSubmit(updateApplication)}> 61 97 <div className="row"> 62 <div className="col-md-6"> 63 <label className="label">Why are you interested in joining our company?</label> 64 <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled 65 placeholder="Write your answer here..." className="application-textarea"/> 66 <br/><br/> 67 <label className="label">What makes you a good fit for this position?</label> 68 <textarea disabled type="text" defaultValue={application.questionAnswers[1]} 69 placeholder="Write your answer here..." className="application-textarea"/> 70 <br/><br/> 71 <label className="label">What do you hope to achieve in your first 6 months in this 72 role?</label> 73 <textarea disabled type="text" defaultValue={application.questionAnswers[2]} 74 placeholder="Write your answer here..." className="application-textarea"/> 98 <div className="col-md-6 d-flex flex-column gap-4"> 99 <div> 100 <label className="label">Why are you interested in joining our company?</label> 101 <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled 102 placeholder="Write your answer here..." className="application-textarea"/> 103 </div> 104 105 <div> 106 <label className="label">What makes you a good fit for this position?</label> 107 <textarea disabled type="text" defaultValue={application.questionAnswers[1]} 108 placeholder="Write your answer here..." className="application-textarea"/> 109 </div> 110 111 <div> 112 <label className="label">What do you hope to achieve in your first 6 months in this 113 role?</label> 114 <textarea disabled type="text" defaultValue={application.questionAnswers[2]} 115 placeholder="Write your answer here..." className="application-textarea"/> 116 </div> 117 75 118 76 119 </div> 77 <div className="col-md-6"> 78 <label htmlFor="start">Curriculum vitae (CV)</label> 79 <br/> 80 <a className="resume-link" href={resumeUrl} target="_blank" 81 rel="noopener noreferrer">{getFileName(application.fileName)}</a> 82 <br/> 120 <div className="col-md-6 d-flex flex-column gap-4"> 121 <div> 122 <label className="label" htmlFor="start">Curriculum vitae (CV)</label> 83 123 84 <br/> 85 <label className="label">Message to the recruiter</label> 86 <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..." 87 className="application-textarea"/> 124 <a className="resume-link" href={resumeUrl} target="_blank" 125 rel="noopener noreferrer">{getFileName(application.fileName)}</a> 126 </div> 127 128 <div> 129 <label className="label">Message to the recruiter</label> 130 <textarea disabled type="text" defaultValue={application.message} 131 placeholder="Optional..." 132 className="application-textarea"/> 133 </div> 134 135 136 {additionalFileUrls.length > 0 ? (<div> 137 <label className="label" htmlFor="start">Additional documents</label> 138 <ul style={{listStyleType: "none", padding: 0, margin: 0}}> 139 {additionalFileUrls.map((url, index) => ( 140 <li style={{marginBottom: 10}} key={index}> 141 <a href={url} className="resume-link" download target="_blank"> 142 {getFileName(url)} 143 </a> 144 </li>))} 145 </ul> 146 </div>) : (<div> 147 {(application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) && 148 <div> 149 <label className="label" htmlFor="start">Additional documents</label> 150 <input 151 className="resume-link" 152 onChange={(e) => setAdditionalFiles(e.target.files)} 153 required type="file" 154 id="fileUpload" 155 accept=".pdf" 156 multiple 157 /> 158 </div>} 159 </div> 160 )} 161 88 162 89 163 </div> 90 164 </div> 165 {(additionalFileUrls.length === 0 && application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) && 166 <div className="modal-buttons"> 167 <div className="cancel-btn" onClick={toggleModal}> Cancel</div> 168 <button className="submit-btn"> Submit</button> 169 </div>} 91 170 92 171 </form> 172 173 93 174 </div> 94 175 </Modal> -
jobvista-frontend/src/views/applications/Applications.css
r0f0add0 r4d97b63 17 17 } 18 18 19 /*.application-title span {*/ 20 /* font-weight: bold;*/ 21 /* */ 22 /*}*/ 19 .response-message { 20 background-color: floralwhite; 21 border-radius: 8px; 22 padding: 15px; 23 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 24 } 25 26 27 28 29 .application-card-wrapper { 30 margin: 15px 0; 31 display: flex; 32 flex-direction: column; 33 transition: all 0.4s ease-in-out; 34 gap: 3px; 35 } 36 37 .application-card.changed { 38 background-color: aliceblue; 39 } 40 41 .application-card-wrapper .expand-section { 42 max-height: 0; 43 opacity: 0; 44 transition: 0.5s ease; 45 display: flex; 46 flex-direction: column; 47 align-items: flex-end; 48 margin: 0 !important; 49 } 50 51 .application-card-wrapper.expanded .expand-section { 52 max-height: 200px; 53 opacity: 1; 54 transform: translateY(0); 55 margin-top: 10px; 56 /* transition: max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;*/ 57 } 58 59 .expand-section textarea { 60 width: 100%; 61 padding: 10px; 62 border-radius: 8px; 63 border: 1px solid #ccc; 64 } 65 23 66 24 67 .application-card { … … 29 72 transition: all 0.3s ease; 30 73 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 31 height: auto;74 /*height: auto;*/ 32 75 padding: 20px 20px; 33 76 display: flex; 34 /*justify-content: center;*/35 /*gap: 20px;*/36 margin: 15px 0;37 /*z-index: -1000;*/38 77 } 39 78 .application-card .app-job-seeker-pic { … … 51 90 52 91 .application-card .app-info { 53 width: 6 5%;92 width: 60%; 54 93 display: inline-flex; 55 94 flex-direction: column; … … 86 125 87 126 .application-card .app-status { 88 width: 28%;127 width: 38%; 89 128 display: inline-flex; 90 129 justify-content: end; … … 98 137 } 99 138 100 . status {139 .app-status .status { 101 140 color: white; 102 141 padding: 5px 10px; … … 105 144 text-align: center; 106 145 } 146 147 148 149 150 151 107 152 108 153 … … 168 213 transform: rotate(-10deg); 169 214 } 215 216 .application-filters { 217 gap: 15px; 218 border-radius: 8px; 219 background-color: white; 220 } 221 222 /* Default Span Styles */ 223 .application-filters span { 224 padding: 8px 12px; 225 border-radius: 5px; 226 color: rgba(1,38,90,0.9); 227 cursor: pointer; 228 display: inline-flex; 229 align-items: center; 230 } 231 232 /* Icon Styles */ 233 .application-filters span i { 234 margin-right: 5px; 235 } 236 237 /* Hover Effect */ 238 .application-filters span:hover { 239 background-color: rgba(1,38,90,0.9); 240 color: white; 241 } 242 243 .application-filters span:hover i { 244 color: inherit; /* Makes the icon inherit the color from the parent span */ 245 } 246 247 /* Selected State */ 248 .application-filters span.selected { 249 background-color: rgba(1,38,90,0.9); 250 color: white; 251 } 252 253 .application-filters span.selected i { 254 color: inherit; /* Ensures icon color matches the selected state */ 255 } -
jobvista-frontend/src/views/applications/ApplicationsByJobAd.js
r0f0add0 r4d97b63 25 25 const [jobAdTitle, setJobAdTitle] = useState(""); 26 26 27 const [changedApplications, setChangedApplications] = useState({}); 28 27 29 useEffect(() => { 28 if(!dispatched && (applicationsByJobAdState.length === 0 || applicationsByJobAdState.length === 1)) {30 if(!dispatched) { 29 31 dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => { 30 32 if (success && reponse.data.length > 0) { … … 60 62 }, [dispatched]) 61 63 64 62 65 const fetchProfilePic = (jobSeekerId) => { 63 66 dispatch(JobSeekerActions.downloadProfilePic(jobSeekerId, (success, response) => { … … 67 70 })) 68 71 } 69 70 72 71 73 const options = [ … … 76 78 ]; 77 79 80 const [selectedFilter, setSelectedFilter] = useState('All'); 81 82 const filters = [ 83 { value: 'ALL', label: 'All', icon: 'fa-folder-open' }, 84 { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' }, 85 { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' }, 86 { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' }, 87 { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' }, 88 ]; 89 90 const filterApplicationsByJobAdvertisement = (filter) => { 91 dispatch(ApplicationActions.filterApplicationsByJobAdId(advertisement_id, filter, (success, response) => { 92 if(success) { 93 //notify 94 } 95 })) 96 } 97 78 98 let handleDefaultStatus = (status) => { 79 99 return options.find(option => option.value === status); 80 100 } 81 101 82 let handleChangeStatus = (selectedOption, id) => { 83 dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => { 102 let handleStatusChange = (selectedOption, id) => { 103 104 const currentApplication = applicationsByJobAd.find(app => app.id === id); 105 106 setChangedApplications(prevState => ({ 107 ...prevState, 108 [id]: { 109 ...prevState[id], 110 response: (selectedOption.value === "ACCEPTED" || selectedOption.value === "DENIED") ? (prevState[id]?.response || currentApplication.response || "") : "", 111 status: selectedOption.value 112 } 113 })) 114 115 setApplicationsByJobAd(prevState => ( 116 prevState.map(application => 117 application.id === id ? {...application, status: selectedOption.value} : application) 118 )) 119 120 const responseTextarea = document.getElementById(`response-${id}`); 121 if (responseTextarea) { 122 responseTextarea.value = ""; 123 } 124 125 /* dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => { 84 126 if(success) { 85 // console.log("Status updated.")86 127 notifyAppStatusUpdate() 87 128 } 88 })) 89 } 129 }))*/ 130 } 131 132 let handleResponseChange = (e, id) => { 133 134 const currentApplication = applicationsByJobAd.find(app => app.id === id); 135 136 setChangedApplications(prevState => ({ 137 ...prevState, 138 [id]: { 139 ...prevState[id], 140 response: e.target.value, 141 status: prevState[id]?.status || currentApplication.status, 142 } 143 } 144 )) 145 } 146 147 const handleSaveChanges = () => { 148 console.log(changedApplications) 149 const changes = Object.entries(changedApplications).map( 150 ([applicationId, change]) => ({ 151 id: applicationId, 152 status: change.status, 153 response: change.response, 154 }) 155 ); 156 console.log(changes) 157 158 if(changes.length === 0) { 159 return; 160 } 161 162 dispatch(ApplicationActions.updateApplications(changes, (success, response) => { 163 if(success) { 164 setChangedApplications({}); 165 notifyAppStatusUpdate() 166 //notify change success 167 } 168 })) 169 170 171 } 172 173 const isChangedApplication = (id) => { 174 return changedApplications && Object.keys(changedApplications).includes(id.toString()); 175 }; 90 176 91 177 … … 98 184 </div> 99 185 186 <div className="row"> 187 <div className="col-md-6 application-filters-wrap"> 188 <div className="application-filters d-inline-flex flex-row justify-content-start"> 189 { 190 filters.map(filter => ( 191 <span 192 key={filter.label} 193 className={selectedFilter === filter.label ? "selected" : ""} 194 onClick={() => { 195 setSelectedFilter(filter.label) 196 filterApplicationsByJobAdvertisement(filter.value) 197 setChangedApplications({}); 198 }} 199 ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span> 200 )) 201 } 202 </div> 203 204 </div> 205 <div className="col-md-6 d-inline-flex flex-row justify-content-end"> 206 <button onClick={handleSaveChanges} 207 className={`blue-submit-button ${Object.keys(changedApplications).length === 0 ? 'disabled' : ''}`} 208 disabled={Object.keys(changedApplications).length === 0} 209 >Submit Changes</button> 210 </div> 211 </div> 212 213 214 100 215 {applicationsByJobAd && applicationsByJobAd.map((application, index) => ( 101 <div className="application-card"> 102 <div className="app-job-seeker-pic"> 103 <img 104 // loading gif 105 src={profilePicsState[application.jobSeekerId]} 106 alt="" 107 width={75} height={75} 108 /> 109 </div> 110 <div className="app-info"> 111 <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 112 day: 'numeric', 113 month: 'long', 114 year: 'numeric' 115 })}</b></span> 116 <div className="contact-info"> 117 <div className="contact-item"> 118 <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span> 119 </div> <div className="contact-item"> 120 <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span> 121 </div> <div className="contact-item"> 122 <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span> 216 <div 217 key={application.id} 218 className={`application-card-wrapper ${(application.status !== "PROPOSED" ) ? 'expanded' : ''}`} 219 > 220 221 <div className={`application-card ${changedApplications[application.id] ? 'changed' : ''}`}> 222 <div className="app-job-seeker-pic"> 223 <img 224 src={profilePicsState[application.jobSeekerId]} 225 alt="" 226 width={75} height={75} 227 /> 228 </div> 229 <div className="app-info"> 230 <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 231 day: 'numeric', 232 month: 'long', 233 year: 'numeric' 234 })}</b></span> 235 <div className="contact-info"> 236 <div className="contact-item"> 237 <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span> 238 </div> 239 <div className="contact-item"> 240 <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span> 241 </div> 242 <div className="contact-item"> 243 <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span> 244 </div> 245 </div> 246 </div> 247 248 <div className="app-status"> 249 <ApplicationDetailsModal application={application} /> 250 <div className="select"> 251 <Select options={options} onChange={(selectedOption) => handleStatusChange(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)} /> 123 252 </div> 124 253 </div> 125 254 </div> 126 255 127 <div className="app-status"> 128 <ApplicationDetailsModal application={application}/> 129 <div className="select"> 130 <Select options={options} onChange={(selectedOption) => handleChangeStatus(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)}/> 131 </div> 132 256 <div className="expand-section"> 257 <textarea 258 id={`response-${application.id}`} 259 placeholder={application.status === "UNDER_REVIEW" ? "Request additional documents..." :"Write your response..."} 260 defaultValue={application.response} 261 onChange={(e) => handleResponseChange(e, application.id)} 262 /> 133 263 </div> 134 264 </div> 135 ))} 265 ))} 266 136 267 137 268 </div>) -
jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js
r0f0add0 r4d97b63 22 22 23 23 24 25 24 useEffect(() => { 26 if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) {25 if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) { 27 26 dispatch(ApplicationActions.fetchApplicationsByJobSeeker(auth.id, (success, response) => { 28 if (success && response.data.length > 0) {27 if (success && response.data.length > 0) { 29 28 setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn")); 30 29 } … … 41 40 useEffect(() => { 42 41 43 if (dispatched && !logoDispatched) {42 if (dispatched && !logoDispatched) { 44 43 applicationsByJobSeeker.forEach(jobAd => { 45 if (jobAd.recruiterId && !logos[jobAd.recruiterId]) {44 if (jobAd.recruiterId && !logos[jobAd.recruiterId]) { 46 45 fetchLogo(jobAd.recruiterId); 47 46 } … … 49 48 setLogoDispatched(true) 50 49 console.log("Fetch all logos GET") 51 } else if (logoDispatched) {50 } else if (logoDispatched) { 52 51 setLogos(logosState) 53 52 console.log("Fetch all logos STATE") … … 58 57 59 58 60 61 59 const fetchLogo = (recruiterId) => { 62 60 dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => { 63 if (success) {61 if (success) { 64 62 setLogos(prevLogos => ({...prevLogos, [recruiterId]: response})) 65 63 } … … 67 65 }; 68 66 69 const options = [ 70 {value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i className="fa-solid fa-paper-plane"></i> Proposed</span>}, 71 {value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i className="fa-solid fa-file-pen"></i> Under Review</span>}, 72 {value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i className="fa-solid fa-user-check"></i> Accepted</span>}, 73 {value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i className="fa-solid fa-user-slash"></i> Denied</span>} 67 const options = [{ 68 value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i 69 className="fa-solid fa-paper-plane"></i> Proposed</span> 70 }, { 71 value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i 72 className="fa-solid fa-file-pen"></i> Under Review</span> 73 }, { 74 value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i 75 className="fa-solid fa-user-check"></i> Accepted</span> 76 }, { 77 value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i 78 className="fa-solid fa-user-slash"></i> Denied</span> 79 }]; 80 81 const [selectedFilter, setSelectedFilter] = useState('All'); 82 83 const filters = [ 84 { value: 'ALL', label: 'All', icon: 'fa-folder-open' }, 85 { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' }, 86 { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' }, 87 { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' }, 88 { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' }, 74 89 ]; 90 91 const filterApplicationsByJobSeeker= (filter) => { 92 dispatch(ApplicationActions.filterApplicationsByJobSeeker(auth.id, filter, (success, response) => { 93 if(success) { 94 //notify 95 } 96 })) 97 } 75 98 76 99 let handleDefaultValue = (status) => { … … 79 102 80 103 81 82 return ( 83 <div className="custom-container"> 104 return (<div className="custom-container"> 84 105 85 106 <div className="application-title"> 86 107 <h3>Application history</h3> 87 108 </div> 109 110 <div className="application-filters d-inline-flex flex-row justify-content-start"> 111 { 112 filters.map(filter => ( 113 <span 114 key={filter.label} 115 className={selectedFilter === filter.label ? "selected" : ""} 116 onClick={() => { 117 setSelectedFilter(filter.label) 118 filterApplicationsByJobSeeker(filter.value) 119 }} 120 ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span> 121 )) 122 } 123 </div> 124 88 125 {applicationsByJobSeeker && applicationsByJobSeeker.map((application, index) => ( 89 <div key={index} className="application-card"> 90 <div className="app-company-logo"> 91 <img 92 // loading gif 93 src={logosState[application.recruiterId]} 94 alt="" 95 width={75} height={75} 96 /> 97 </div> 126 <div className="application-card-wrapper"> 127 <div key={index} className="application-card"> 128 <div className="app-company-logo"> 129 <img 130 // loading gif 131 src={logosState[application.recruiterId]} 132 alt="" 133 width={75} height={75} 134 /> 135 </div> 98 136 99 <div className="app-info"> 100 <Link to={`/job-advertisements/${application.jobAdId}`} className="jobAd-title">{application.jobAdTitle}</Link> 101 {/*<h5 className="jobAd-title"></h5>*/} 102 <div className="contact-info"> 103 <div className="contact-item"> 104 <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span> 137 <div className="app-info"> 138 <Link to={`/job-advertisements/${application.jobAdId}`} 139 className="jobAd-title">{application.jobAdTitle}</Link> 140 {/*<h5 className="jobAd-title"></h5>*/} 141 <div className="contact-info"> 142 <div className="contact-item"> 143 <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span> 144 </div> 145 <div className="contact-item"> 146 <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span> 147 </div> 148 <div className="contact-item"> 149 <i className="fa-solid fa-phone"></i> 150 <span>{application.recruiterPhoneNumber}</span> 151 </div> 152 <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 153 day: 'numeric', month: 'long', year: 'numeric' 154 })}</b></span> 105 155 </div> 106 <div className="contact-item"> 107 <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span> 108 </div> 109 <div className="contact-item"> 110 <i className="fa-solid fa-phone"></i> <span>{application.recruiterPhoneNumber}</span> 111 </div> 112 <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', { 113 day: 'numeric', 114 month: 'long', 115 year: 'numeric' 116 })}</b></span> 156 </div> 157 158 <div className="app-status"> 159 <ApplicationDetailsModal application={application}/> 160 <> {handleDefaultValue(application.status).label}</> 161 {/*<div className="select">*/} 162 {/* <Select isDisabled={true} options={options} />*/} 163 {/*</div>*/} 164 117 165 </div> 118 166 </div> 167 {application.response && 168 <div className="response-message"> 169 {application.response} 170 </div> 171 } 119 172 120 <div className="app-status"> 121 <ApplicationDetailsModal application={application}/> 122 <> {handleDefaultValue(application.status).label}</> 123 {/*<div className="select">*/} 124 {/* <Select isDisabled={true} options={options} />*/} 125 {/*</div>*/} 173 </div> 126 174 127 </div>128 </div>129 175 ))} 130 176 131 </div> 132 ) 177 </div>) 133 178 } -
jobvista-frontend/src/views/applications/ApplyToJobAdModal.js
r0f0add0 r4d97b63 105 105 onChange={(e) => setResumeFile(e.target.files[0])} required type="file" 106 106 id="fileUpload" accept=".pdf"/> 107 108 107 <br/> 109 108 <label className="label">Message to the recruiter</label> -
jobvista-frontend/src/views/auth/SignInForm.js
r0f0add0 r4d97b63 1 import {Button, TextField} from "@mui/material";2 1 import {Link} from "react-router-dom"; 3 2 import "./auth.css" … … 9 8 import {AuthActions} from "../../redux/actions/authActions"; 10 9 import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils"; 10 11 import {GoogleOAuthProvider, GoogleLogin} from "@react-oauth/google"; 11 12 12 13 export const SignInForm = () => { … … 40 41 } 41 42 43 const handleGoogleSuccess = (response) => { 44 const tokenId = response.credential; 45 46 dispatch(AuthActions.signInGoogle(tokenId, (success, error) => { 47 if (success) { 48 console.log("User signed in successfully"); 49 if(success) { 50 navigate("/") 51 } 52 } else { 53 console.error("Google sign-in failed", error); 54 } 55 })); 56 }; 57 58 const handleGoogleFailure = (error) => { 59 console.error(error); 60 }; 61 42 62 return ( 43 63 44 <div className=" d-flex align-items-center">64 <div className=""> 45 65 <div className="container"> 46 <div className="row ">47 <div className="col-md-8 mx-autoform-container">66 <div className="row d-flex flex-column justify-content-center align-items-center"> 67 <div className="col-md-8 form-container"> 48 68 <h3 className="login-heading mb-4">Sign in</h3> 49 69 <form onSubmit={handleSubmit(signIn)}> … … 83 103 84 104 <div className="row"> 105 <GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}> 106 <GoogleLogin 107 onSuccess={handleGoogleSuccess} 108 onError={handleGoogleFailure} 109 type={"standard"} 110 text={"signin_with"} 111 locale={"en"} 112 redirectUri="http://localhost:3000/login/oauth2/code/google" 113 /> 114 </GoogleOAuthProvider> 115 </div> 116 <br/> 117 </div> 118 119 <div className="col-md-8 mt-5 form-container"> 120 <div> 121 <h5 className="mb-3">Don't have an account?</h5> 122 </div> 123 124 <div className="row"> 85 125 <div className="col-md-6"> 86 126 <Link to="/signup/recruiter" className="btn auth-secondary-btn text-uppercase fw-bold mb-2 w-100">SIGN UP AS RECRUITER</Link> … … 91 131 </div> 92 132 </div> 133 93 134 </div> 94 135 </div> -
jobvista-frontend/src/views/auth/auth.css
r0f0add0 r4d97b63 4 4 } 5 5 6 .form-container {7 margin-bottom: 80px;8 }9 6 10 7 .auth-primary-btn{ … … 27 24 color: white; 28 25 } 26 27 iframe{ 28 margin: auto !important; 29 } -
jobvista-frontend/src/views/dashboard/Dashboard.js
r0f0add0 r4d97b63 141 141 </div> 142 142 <div className="card-body"> 143 <img 143 <img className="card-company-logo" 144 144 // loading gif 145 145 src={logos[jobAd.recruiterId]} -
jobvista-frontend/src/views/job_advertisements/JobAdDetails.js
r0f0add0 r4d97b63 85 85 <> 86 86 <img 87 className="card-company-logo" 87 88 // loading gif 88 89 src={logosState[jobAd.recruiterId]} -
jobvista-frontend/src/views/shared_css/Modal.css
r0f0add0 r4d97b63 65 65 .react-responsive-modal-modal .modal-content form .label { 66 66 display: block; 67 margin-bottom: 10px; 68 font-weight: 500; 67 69 } 68 70 -
jobvista-frontend/src/views/static/Header.js
r0f0add0 r4d97b63 117 117 {user.role === Roles.RECRUITER && <img src={logoState[auth.id]} /> } 118 118 {user.role === Roles.ADMIN && <img src="/images/admin.jpg"/> } 119 {/*<img src="https://lh3.googleusercontent.com/a/ACg8ocJOmmRzyRWcuhJj_sCzIoxMeP1M1DOgQ1UeYsFoeJuFB4XgOAnS=s96-c"/>*/} 119 120 </div> 120 121 <div className="auth-box">
Note:
See TracChangeset
for help on using the changeset viewer.