Changeset 4d97b63


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

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

Files:
20 added
36 edited

Legend:

Unmodified
Added
Removed
  • jobvista-backend/.gitignore

    r0f0add0 r4d97b63  
    3232### VS Code ###
    3333.vscode/
     34
     35.env
  • jobvista-backend/pom.xml

    r0f0add0 r4d97b63  
    6363                        <scope>test</scope>
    6464                </dependency>
     65                <!-- other -->
     66                <dependency>
     67                        <groupId>org.springframework.boot</groupId>
     68                        <artifactId>spring-boot-starter-oauth2-client</artifactId>
     69                </dependency>
    6570
    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
    67105                <!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
    68106                <dependency>
     
    71109                        <version>3.0.2</version>
    72110                </dependency>
    73 
    74 
    75111
    76112                <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/JobvistaBackendApplication.java

    r0f0add0 r4d97b63  
    2727                        admin.setEmail("admin@admin.com");
    2828                        admin.setHasAccess(true);
    29 //                      admin.setName("admin");
    30 //                      admin.setSurname("admin");
    3129                        admin.setPassword(new BCryptPasswordEncoder().encode("admin"));
    3230                        userRepository.save(admin);
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/config/SecurityConfiguration.java

    r0f0add0 r4d97b63  
    3333                        .requestMatchers(
    3434                                "/api/auth/**",
     35                                "/oauth2/**",
    3536                                "/api/job-advertisements/**",
    3637                                "/api/applications/**",
    3738                                "/api/recruiter/**",
    38                                 "/api/job-seeker/**"
     39                                "/api/job-seeker/**",
     40                                "/uploads/**"
    3941                        ).permitAll()
    4042                        .requestMatchers("/api/admin/**").hasAnyAuthority(Role.ROLE_ADMIN.name())
     
    4648                        .requestMatchers("/api/job-advertisements/edit/{id}").hasAnyAuthority(Role.ROLE_RECRUITER.name())
    4749                        .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())
    4953                        .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())
    5055                        .requestMatchers("/api/applications/submit").hasAnyAuthority(Role.ROLE_JOBSEEKER.name())
    5156                        .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())
    5359                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    5460                .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
    5767        return http.build();
    5868    }
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/ApplicationController.java

    r0f0add0 r4d97b63  
    3030    }
    3131
     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
    3238    @GetMapping("/job-advertisements/{advertisement_id}/applications")
    3339    public ResponseEntity<?> findAllApplicationsByJobAdvertisementId(@PathVariable("advertisement_id") Long advertisementId) {
     
    3642    }
    3743
    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")
    3951    public ResponseEntity<?> updateApplicationStatus(@PathVariable("id") Long applicaitonId, @RequestBody ApplicationStatusDTO appStatusDTO) {
    4052        ApplicationStatusDTO applicationStatusDTO = applicationService.updateApplicationStatus(applicaitonId,appStatusDTO.getStatus());
    4153        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);
    4260    }
    4361
     
    6684        return new ResponseEntity<>(applicationDetailsDTO, HttpStatus.OK);
    6785    }
     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
    68101}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/controllers/AuthController.java

    r0f0add0 r4d97b63  
    77import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.JobSeekerMapper;
    88import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.mappers.RecruiterMapper;
     9
    910import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AuthService;
     11
    1012import org.springframework.http.HttpStatus;
    1113import org.springframework.http.ResponseEntity;
    1214import org.springframework.web.bind.annotation.*;
     15
     16import java.util.Map;
     17
     18
    1319
    1420@RestController
     
    4248        return ResponseEntity.ok(authenticationService.refreshToken(refreshTokenRequest));
    4349    }
     50
     51    @PostMapping("/google")
     52    public ResponseEntity<?> googleSignIn(@RequestBody Map<String, String> token) {
     53        return ResponseEntity.ok(authenticationService.googleSignIn(token));
     54    }
    4455}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/Application.java

    r0f0add0 r4d97b63  
    99import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.job_advertisements.JobAdvertisement;
    1010import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker;
    11 import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User;
    1211
    1312import java.time.LocalDateTime;
    14 import java.util.HashMap;
     13import java.util.ArrayList;
    1514import java.util.List;
    1615
     
    4544    private ApplicationStatus status;
    4645
     46    private String response;
     47
     48    @ElementCollection
     49    private List<String> additionalFilePaths;
     50
    4751    public Application(JobSeeker jobSeeker, JobAdvertisement jobAdvertisement, List<String> answers, String message) {
    4852        this.jobSeeker = jobSeeker;
     
    5357        submittedOn = LocalDateTime.now();
    5458        this.status = ApplicationStatus.PROPOSED;
     59        this.response = "";
     60        this.additionalFilePaths = new ArrayList<>();
    5561    }
    5662
     
    7278                application.getMessage(),
    7379                application.getSubmittedOn(),
    74                 application.getStatus().name()
     80                application.getStatus().name(),
     81                application.getResponse(),
     82                application.getAdditionalFilePaths()
    7583        );
    7684    }
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationDetailsDTO.java

    r0f0add0 r4d97b63  
    2929    private LocalDateTime submittedOn;
    3030    private String status;
     31    private String response;
     32    private List<String> additionalFileNames;
    3133}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/models/applications/DTO/ApplicationStatusDTO.java

    r0f0add0 r4d97b63  
    99    Long id;
    1010    String status;
     11    String response;
    1112}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/repositories/JobSeekerRepository.java

    r0f0add0 r4d97b63  
    44import org.springframework.data.jpa.repository.JpaRepository;
    55
     6import java.util.Optional;
     7
    68public interface JobSeekerRepository extends JpaRepository<JobSeeker, Long> {
     9    Optional<JobSeeker> findByEmail(String email);
    710}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/ApplicationServiceImpl.java

    r0f0add0 r4d97b63  
    66import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.JobSeeker;
    77import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.JobSeekerRepository;
     8import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.EmailSenderService;
    89import org.springframework.core.io.Resource;
    910import org.springframework.core.io.UrlResource;
     
    1920import org.springframework.beans.factory.annotation.Value;
    2021import org.springframework.stereotype.Service;
     22import org.springframework.web.multipart.MultipartFile;
     23import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
    2124
    2225import java.io.IOException;
     26import java.net.MalformedURLException;
    2327import java.nio.file.Files;
    2428import java.nio.file.Path;
     
    3943
    4044    @Autowired
     45    private EmailSenderService emailSenderService;
     46
     47    @Autowired
    4148    public ApplicationServiceImpl(@Value("${file.upload-dir}") String uploadDir, UserRepository userRepository, ApplicationRepository applicationRepository, JobAdvertisementRepository jobAdvertisementRepository,
    4249                                  JobSeekerRepository jobSeekerRepository) {
     
    95102
    96103    @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
    97138    public List<ApplicationDetailsDTO> findAllByJobAdvertisementId(Long jobId) {
    98139        List<Application> applications =  applicationRepository.findAllByJobAdvertisementId(jobId);
     
    101142
    102143    @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
    103157    public List<ApplicationDetailsDTO> findAllByJobSeekerId(Long jobSeekerId) {
    104158       List<Application> applications = applicationRepository.findAllByJobSeekerId(jobSeekerId);
    105159       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();
    106172    }
    107173
     
    126192    }
    127193
     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
    128282    @Override
    129283    public ApplicationStatusDTO updateApplicationStatus(Long id, String status) {
     
    132286       application.setStatus(ApplicationStatus.valueOf(status));
    133287       applicationRepository.save(application);
    134        return new ApplicationStatusDTO(id, status);
     288       return new ApplicationStatusDTO(id, status, "");
    135289    }
    136290}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/impl/AuthServiceImpl.java

    r0f0add0 r4d97b63  
    11package mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.impl;
    22
     3import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
     4import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
     5import com.google.api.client.http.javanet.NetHttpTransport;
     6import com.google.api.client.json.jackson2.JacksonFactory;
    37import lombok.RequiredArgsConstructor;
     8import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.config.GoogleOAuth2Properties;
     9import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.controllers.AuthController;
     10import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.enumerations.Role;
    411import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.SignInDTO;
    512import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.DTO.JwtAuthResponse;
     
    1219import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.repositories.UserRepository;
    1320import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.AuthService;
     21import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JobSeekerService;
    1422import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.service.intef.JwtService;
    1523import org.springframework.security.authentication.AuthenticationManager;
    1624import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
     25import org.springframework.security.core.authority.SimpleGrantedAuthority;
    1726import org.springframework.security.crypto.password.PasswordEncoder;
     27import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
     28import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
     29import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
     30import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
     31import org.springframework.security.oauth2.core.user.OAuth2User;
    1832import org.springframework.stereotype.Service;
    19 
     33import org.springframework.web.multipart.MultipartFile;
     34
     35import javax.imageio.ImageIO;
     36import java.awt.image.BufferedImage;
     37import java.io.ByteArrayInputStream;
     38import java.io.ByteArrayOutputStream;
     39import java.io.IOException;
     40import java.io.InputStream;
     41import java.net.URL;
    2042import java.time.LocalDateTime;
     43import java.util.Collections;
    2144import java.util.HashMap;
     45import java.util.Map;
     46import java.util.Optional;
    2247
    2348@Service
     
    3055    private final AuthenticationManager authenticationManager;
    3156    private final UserRepository userRepository;
     57    private final JobSeekerService jobSeekerService;
    3258    private final JwtService jwtService;
     59    private final GoogleOAuth2Properties googleOAuth2Properties;
    3360
    3461    @Override
     
    5683        return new JwtAuthResponse(user.getId(), user.getEmail(), user.getName(), user.getRole().name(), user.isHasAccess(), jwt, refreshJwt);
    5784    }
    58    
     85
    5986    public JwtAuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest) {
    6087        String userEmail = jwtService.extractUsername(refreshTokenRequest.getToken());
    6188        User user = userRepository.findByEmail(userEmail).orElseThrow();
    62         if(jwtService.isTokenValid(refreshTokenRequest.getToken(), user)) {
     89        if (jwtService.isTokenValid(refreshTokenRequest.getToken(), user)) {
    6390            String jwt = jwtService.generateToken(user);
    6491
     
    6794        return null;
    6895    }
     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    }
    69250}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/ApplicationService.java

    r0f0add0 r4d97b63  
    77import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.applications.DTO.ApplicationStatusDTO;
    88import org.springframework.core.io.Resource;
     9import org.springframework.web.multipart.MultipartFile;
    910
    1011import java.util.List;
     
    1213public interface ApplicationService {
    1314    ApplicationDetailsDTO submitApplication(ApplicationDTO applicationDTO);
     15    ApplicationDetailsDTO updateApplication(Long applicationId, MultipartFile[] additionalFiles);
    1416    List<ApplicationDetailsDTO> findAllByJobAdvertisementId(Long jobId);
     17    List<ApplicationDetailsDTO> filterByJobAdvertisementId(Long jobId, String status);
    1518    List<ApplicationDetailsDTO> findAllByJobSeekerId(Long jobSeekerId);
     19    List<ApplicationDetailsDTO> filterByJobSeekerId(Long jobSeekerId, String status);
    1620    Resource loadResumeAsResource(Long applicationId);
     21    List<String> loadAdditionalFilesAsUrls(Long applicationId);
    1722    ApplicationStatusDTO updateApplicationStatus(Long id, String status);
     23    List<ApplicationStatusDTO> updateApplications(List<ApplicationStatusDTO> updates);
    1824}
  • jobvista-backend/src/main/java/mk/ukim/finki/predmeti/internettehnologii/jobvistabackend/service/intef/AuthService.java

    r0f0add0 r4d97b63  
    77import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.Recruiter;
    88import mk.ukim.finki.predmeti.internettehnologii.jobvistabackend.models.users.User;
     9import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
     10
     11import java.util.Map;
    912
    1013public interface AuthService {
     
    1316    JwtAuthResponse signIn(SignInDTO signInDTO);
    1417    JwtAuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest);
     18
     19    JwtAuthResponse googleSignIn(Map<String, String> token);
     20    OAuth2AuthenticationToken getAuthentication(String tokenId);
     21    void submitGoogleProfilePic(Long jobSeekerId, String googleProfilePicUrl);
    1522}
  • jobvista-backend/src/main/resources/application.properties

    r0f0add0 r4d97b63  
    22
    33spring.datasource.driver-class-name=org.postgresql.Driver
    4 spring.datasource.url=jdbc:postgresql://localhost:5432/jobvistaDB
    5 spring.datasource.username=postgres
    6 spring.datasource.password=postgres
     4spring.datasource.url=${db_url}
     5spring.datasource.username=${db_username}
     6spring.datasource.password=${db_password}
    77
    88#spring.jpa.hibernate.ddl-auto=create-drop
     
    1212spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
    1313
    14 file.upload-dir=./uploads
     14file.upload-dir=${file_upload_dir}
    1515
    1616spring.servlet.multipart.enabled=true
    1717spring.servlet.multipart.max-file-size=2MB
    1818spring.servlet.multipart.max-request-size=2MB
     19
     20spring.security.oauth2.client.registration.google.client-id=${google_id}
     21spring.security.oauth2.client.registration.google.client-secret=${google_secret}
     22spring.security.oauth2.client.registration.google.scope=profile, email
     23spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:3000/login/oauth2/code/google
     24spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
     25spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
     26spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
     27spring.security.oauth2.client.provider.google.user-name-attribute=sub
     28
     29spring.mail.host=smtp.gmail.com
     30spring.mail.port=587
     31spring.mail.username=${mail_username}
     32spring.mail.password=${mail_password}
     33custom.mail.sender.email=${mail_sender_email}
     34custom.mail.sender.name=${mail_sender_name}
     35spring.mail.properties.mail.smtp.auth=true
     36spring.mail.properties.mail.smtp.starttls.enable=true
     37
     38
  • jobvista-frontend/.gitignore

    r0f0add0 r4d97b63  
    2222yarn-debug.log*
    2323yarn-error.log*
     24
     25.env
  • jobvista-frontend/package-lock.json

    r0f0add0 r4d97b63  
    1313        "@hookform/resolvers": "^3.3.4",
    1414        "@mui/material": "^5.15.17",
     15        "@react-oauth/google": "^0.12.1",
    1516        "@reduxjs/toolkit": "^2.2.3",
    1617        "@testing-library/jest-dom": "^5.17.0",
     
    37893790      }
    37903791    },
     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    },
    37913801    "node_modules/@reduxjs/toolkit": {
    37923802      "version": "2.2.3",
  • jobvista-frontend/package.json

    r0f0add0 r4d97b63  
    88    "@hookform/resolvers": "^3.3.4",
    99    "@mui/material": "^5.15.17",
     10    "@react-oauth/google": "^0.12.1",
    1011    "@reduxjs/toolkit": "^2.2.3",
    1112    "@testing-library/jest-dom": "^5.17.0",
  • jobvista-frontend/public/index.html

    r0f0add0 r4d97b63  
    3232    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
    3333    <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>
    3437    <title>Job Vista</title>
    3538  </head>
  • jobvista-frontend/src/App.css

    r0f0add0 r4d97b63  
    3838
    3939.form-container {
     40  font-family: "Segoe UI";
    4041  background-color: white;
    4142  border-radius: 10px;
    4243  padding: 15px 30px;
    43   margin-top: 80px;
     44  margin-top: 40px;
     45}
     46
     47
     48.form-container h5, .form-container h3 {
     49  text-align:center;
     50  font-family: Poppins
    4451}
    4552
     
    217224}
    218225
    219 
    220 
    221 
    222 
     226.card-company-logo {
     227  border-radius: 15%
     228}
     229
     230
     231
     232
     233
  • jobvista-frontend/src/redux/actionTypes.js

    r0f0add0 r4d97b63  
    1313export const FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER = "FILTER_JOB_ADVERTISEMENTS_BY_RECRUITER"
    1414export const SUBMIT_APPLICATION = "SUBMIT_APPLICATION"
     15export const UPDATE_APPLICATION = "UPDATE_APPLICATION"
    1516export const UPDATE_APPLICATION_STATUS = "UPDATE_APPLICATION_STATUS"
     17export const UPDATE_APPLICATIONS = "UPDATE_APPLICATIONS"
    1618export const FETCH_APPLICATIONS_BY_JOB_ID = "FETCH_APPLICATIONS_BY_JOB_ID"
     19export const FILTER_APPLICATIONS_BY_JOB_ID = "FILTER_APPLICATIONS_BY_JOB_ID"
    1720export const FETCH_APPLICATIONS_BY_JOB_SEEKER_ID = "FETCH_APPLICATIONS_BY_JOB_SEEKER_ID"
     21export const FILTER_APPLICATIONS_BY_JOB_SEEKER_ID = "FILTER_APPLICATIONS_BY_JOB_SEEKER_ID"
    1822export const DOWNLOAD_RESUME = "DOWNLOAD_RESUME"
    1923
  • jobvista-frontend/src/redux/actions/applicationActions.js

    r0f0add0 r4d97b63  
    33    CURRENT_USER,
    44    FETCH_APPLICATIONS_BY_JOB_ID,
    5     FETCH_APPLICATIONS_BY_JOB_SEEKER_ID,
    6     SUBMIT_APPLICATION, UPDATE_APPLICATION_STATUS
     5    FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID,
     6    SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS, UPDATE_APPLICATIONS
    77} from "../actionTypes";
    88
     
    1818                    dispatch({
    1919                        type: SUBMIT_APPLICATION,
     20                        application: response.data
     21                    })
     22                    callback(true, response)
     23                }).catch(error => {
     24                callback(false, error)
     25                console.log(error)
     26            })
     27        }
     28    },
     29
     30    updateApplication: (applicationId, additionalFiles, callback) => {
     31        console.log(additionalFiles)
     32        return dispatch => {
     33            axios.post("/applications/"+ applicationId + "/update", additionalFiles, {
     34                headers: {
     35                    'Content-Type': 'multipart/form-data'
     36                }
     37            })
     38                .then(response => {
     39                    dispatch({
     40                        type: UPDATE_APPLICATION,
    2041                        application: response.data
    2142                    })
     
    4566        }
    4667    },
     68
     69    updateApplications: (changes, callback) => {
     70        return dispatch => {
     71            axios.post("/applications/update", changes)
     72                .then(response => {
     73                    dispatch({
     74                        type: UPDATE_APPLICATIONS,
     75                        applications: response.data
     76                    })
     77                    callback(true, response)
     78                }).catch(error => {
     79                callback(false, error)
     80            })
     81        }
     82    },
     83
    4784    fetchApplicationsByJobSeeker: (jobSeekerId, callback) => {
    4885        return dispatch => {
     
    5188                    dispatch({
    5289                        type: FETCH_APPLICATIONS_BY_JOB_SEEKER_ID,
     90                        applicationsByJobSeeker: response.data
     91                    })
     92                    callback(true, response)
     93                }).catch(error => {
     94                callback(false, error)
     95            })
     96        }
     97    },
     98
     99    filterApplicationsByJobSeeker: (jobSeekerId, status, callback) => {
     100        return dispatch => {
     101            axios.post("/my-applications/" + jobSeekerId + "/filtered", status)
     102                .then(response => {
     103                    dispatch({
     104                        type: FILTER_APPLICATIONS_BY_JOB_SEEKER_ID,
    53105                        applicationsByJobSeeker: response.data
    54106                    })
     
    75127        }
    76128    },
     129
     130    filterApplicationsByJobAdId: (jobAdId, status, callback) => {
     131        return dispatch => {
     132            axios.post("/job-advertisements/" + jobAdId + "/applications/filtered", status)
     133                .then(response => {
     134                        dispatch({
     135                            type: FILTER_APPLICATIONS_BY_JOB_ID,
     136                            applicationsByJobAdId: response.data
     137                        })
     138                        callback(true, response)
     139                    }
     140                ).catch(error => {
     141                callback(false, error)
     142            })
     143        }
     144    },
    77145    downloadResume: (id, callback) => {
    78146        return axios.get("/applications/" + id + "/download-resume", {responseType: "blob"})
     
    86154            })
    87155
     156    },
     157    downloadAdditionalFiles: (id, callback) => {
     158        return axios.get("/applications/" + id + "/download-additional-files")
     159            .then(response => {
     160                const urls = response.data; // This will be a list of URLs
     161                callback(true, urls);
     162            })
     163            .catch(error => {
     164                callback(false, error);
     165            });
     166
    88167    }
    89168}
  • jobvista-frontend/src/redux/actions/authActions.js

    r0f0add0 r4d97b63  
    3232            }).catch((error) => {
    3333                callback(false, error);
     34            });
     35        };
     36    },
     37
     38    signInGoogle: (tokenId, callback) => {
     39        return dispatch => {
     40            axios.post("/auth/google", { tokenId })
     41                .then(jwtResponse => {
     42                    const response = jwtResponse.data;
     43                    const token = response.token;
     44                    const user = {
     45                        name: response.name,
     46                        role: response.role,
     47                        access: response.hasAccess,
     48                        id: response.id,
     49                    };
     50                    dispatch({
     51                        type: SIGN_IN,
     52                        payload: {
     53                            token,
     54                            user
     55                        }
     56                    });
     57                    callback && callback(true);
     58                }).catch(error => {
     59                callback && callback(false, error);
    3460            });
    3561        };
  • jobvista-frontend/src/redux/reducers/applicationReducer.js

    r0f0add0 r4d97b63  
    22    CURRENT_USER,
    33    FETCH_APPLICATIONS_BY_JOB_ID,
    4     FETCH_APPLICATIONS_BY_JOB_SEEKER_ID,
    5     SUBMIT_APPLICATION, UPDATE_APPLICATION_STATUS
     4    FETCH_APPLICATIONS_BY_JOB_SEEKER_ID, FILTER_APPLICATIONS_BY_JOB_ID, FILTER_APPLICATIONS_BY_JOB_SEEKER_ID,
     5    SUBMIT_APPLICATION, UPDATE_APPLICATION, UPDATE_APPLICATION_STATUS
    66} from "../actionTypes";
    77
     
    2222                applicationsByJobSeeker: [...state.applicationsByJobSeeker, action.application]
    2323            }
     24        case UPDATE_APPLICATION:
     25            return {
     26                ...state,
     27                applicationsByJobSeeker: state.applicationsByJobSeeker.map(application =>
     28                    application.id === action.application.id ?
     29                        action.application : // Replace with the updated application
     30                        application // Keep the old one
     31                )
     32            }
    2433        case UPDATE_APPLICATION_STATUS:
    2534            return {
     
    3645                applicationsByJobAdId: action.applicationsByJobAdId
    3746            }
     47        case FILTER_APPLICATIONS_BY_JOB_ID:
     48            return {
     49                ...state,
     50                applicationsByJobAdId: action.applicationsByJobAdId
     51            }
     52
    3853        case FETCH_APPLICATIONS_BY_JOB_SEEKER_ID:
     54            return {
     55                ...state,
     56                applicationsByJobSeeker: action.applicationsByJobSeeker
     57            }
     58        case FILTER_APPLICATIONS_BY_JOB_SEEKER_ID:
    3959            return {
    4060                ...state,
  • jobvista-frontend/src/utils/toastUtils.js

    r0f0add0 r4d97b63  
    108108    toast.success(
    109109        <span>
    110             Status updated successfully!
     110            Application/s updated successfully!
    111111        </span>, {
    112112            position: "bottom-right",
     
    126126        <span>
    127127            Your application was successfully sent!
     128        </span>, {
     129            position: "bottom-right",
     130            autoClose: 5000,
     131            hideProgressBar: false,
     132            closeOnClick: true,
     133            pauseOnHover: false,
     134            draggable: true,
     135            progress: undefined,
     136            theme: "dark",
     137            pauseOnFocusLoss: false
     138        });
     139}
     140
     141export const notifyJobAdUpdate= () => {
     142    toast.success(
     143        <span>
     144            Your application was successfully updated!
    128145        </span>, {
    129146            position: "bottom-right",
  • jobvista-frontend/src/views/applications/ApplicationDetailsModal.js

    r0f0add0 r4d97b63  
    1717import Roles from "../../enumerations/Roles";
    1818import {ApplicationActions} from "../../redux/actions/applicationActions";
     19import {notifyJobAdApply, notifyJobAdUpdate} from "../../utils/toastUtils";
    1920
    2021
     
    2526    const auth = useSelector(state => state.auth.currentUser)
    2627    const [resumeUrl, setResumeUrl] = useState("");
     28    const [additionalFileUrls, setAdditionalFileUrls] = useState([]);
    2729
    28     //const [resumeFile, setResumeFile] = useState(null);
     30    const [additionalFiles, setAdditionalFiles] = useState(null);
    2931    const toggleModal = () => {
    3032        setModal(!modal);
    3133    };
     34
     35    const {register, handleSubmit, control, formState: {errors}} = useForm();
    3236
    3337    useEffect(() => {
     
    3640                if (success) {
    3741                    setResumeUrl(response);
     42
     43                    if (application.additionalFileNames.length > 0) {
     44                        ApplicationActions.downloadAdditionalFiles(application.id, (success2, response) => {
     45                            if (success2) {
     46                                setAdditionalFileUrls(response);
     47                            }
     48                        })
     49                    }
    3850                }
    3951            })
    4052        }
    4153    }, [])
     54
     55    const updateApplication = async () => {
     56        try {
     57            const formData = new FormData();
     58            if (additionalFiles && additionalFiles.length > 0) {
     59                for (let i = 0; i < additionalFiles.length; i++) {
     60                    formData.append('additionalFiles', additionalFiles[i]);
     61                }
     62            }
     63
     64            dispatch(ApplicationActions.updateApplication(application.id, formData, (success) => {
     65                if (success) {
     66                    toggleModal()
     67                    window.location.reload()
     68                }
     69            }))
     70        } catch (err) {
     71            console.error(err)
     72        }
     73    }
    4274
    4375    function getFileName(path) {
     
    5082
    5183    return (<div className="modal-wrap">
    52         <button onClick={toggleModal} className="application-button">View application</button>
     84        {auth.role === Roles.RECRUITER ? <button onClick={toggleModal} className="application-button">View
     85            application</button> : (application.status === "UNDER_REVIEW" && application.response.length > 0 && additionalFileUrls.length === 0) ?
     86            <button onClick={toggleModal} className="application-button">Update application</button> :
     87            <button onClick={toggleModal} className="application-button">View application</button>}
     88
    5389        <Modal open={modal} onClose={toggleModal} center>
    5490            <div className="head-modal">
     
    5894
    5995            <div className="modal-content">
    60                 <form>
     96                <form onSubmit={handleSubmit(updateApplication)}>
    6197                    <div className="row">
    62                         <div className="col-md-6">
    63                             <label className="label">Why are you interested in joining our company?</label>
    64                             <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled
    65                                       placeholder="Write your answer here..." className="application-textarea"/>
    66                             <br/><br/>
    67                             <label className="label">What makes you a good fit for this position?</label>
    68                             <textarea disabled type="text" defaultValue={application.questionAnswers[1]}
    69                                       placeholder="Write your answer here..." className="application-textarea"/>
    70                             <br/><br/>
    71                             <label className="label">What do you hope to achieve in your first 6 months in this
    72                                 role?</label>
    73                             <textarea disabled type="text" defaultValue={application.questionAnswers[2]}
    74                                       placeholder="Write your answer here..." className="application-textarea"/>
     98                        <div className="col-md-6 d-flex flex-column gap-4">
     99                            <div>
     100                                <label className="label">Why are you interested in joining our company?</label>
     101                                <textarea disabled type="text" defaultValue={application.questionAnswers[0]} disabled
     102                                          placeholder="Write your answer here..." className="application-textarea"/>
     103                            </div>
     104
     105                            <div>
     106                                <label className="label">What makes you a good fit for this position?</label>
     107                                <textarea disabled type="text" defaultValue={application.questionAnswers[1]}
     108                                          placeholder="Write your answer here..." className="application-textarea"/>
     109                            </div>
     110
     111                            <div>
     112                                <label className="label">What do you hope to achieve in your first 6 months in this
     113                                    role?</label>
     114                                <textarea disabled type="text" defaultValue={application.questionAnswers[2]}
     115                                          placeholder="Write your answer here..." className="application-textarea"/>
     116                            </div>
     117
    75118
    76119                        </div>
    77                         <div className="col-md-6">
    78                             <label htmlFor="start">Curriculum vitae (CV)</label>
    79                             <br/>
    80                             <a className="resume-link" href={resumeUrl} target="_blank"
    81                                rel="noopener noreferrer">{getFileName(application.fileName)}</a>
    82                             <br/>
     120                        <div className="col-md-6 d-flex flex-column gap-4">
     121                            <div>
     122                                <label className="label" htmlFor="start">Curriculum vitae (CV)</label>
    83123
    84                             <br/>
    85                             <label className="label">Message to the recruiter</label>
    86                             <textarea disabled type="text" defaultValue={application.message} placeholder="Optional..."
    87                                       className="application-textarea"/>
     124                                <a className="resume-link" href={resumeUrl} target="_blank"
     125                                   rel="noopener noreferrer">{getFileName(application.fileName)}</a>
     126                            </div>
     127
     128                            <div>
     129                                <label className="label">Message to the recruiter</label>
     130                                <textarea disabled type="text" defaultValue={application.message}
     131                                          placeholder="Optional..."
     132                                          className="application-textarea"/>
     133                            </div>
     134
     135
     136                            {additionalFileUrls.length > 0 ? (<div>
     137                                    <label className="label" htmlFor="start">Additional documents</label>
     138                                    <ul style={{listStyleType: "none", padding: 0, margin: 0}}>
     139                                        {additionalFileUrls.map((url, index) => (
     140                                            <li style={{marginBottom: 10}} key={index}>
     141                                                <a href={url} className="resume-link" download target="_blank">
     142                                                    {getFileName(url)}
     143                                                </a>
     144                                            </li>))}
     145                                    </ul>
     146                                </div>) : (<div>
     147                                    {(application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) &&
     148                                        <div>
     149                                            <label className="label" htmlFor="start">Additional documents</label>
     150                                            <input
     151                                                className="resume-link"
     152                                                onChange={(e) => setAdditionalFiles(e.target.files)}
     153                                                required type="file"
     154                                                id="fileUpload"
     155                                                accept=".pdf"
     156                                                multiple
     157                                            />
     158                                        </div>}
     159                                </div>
     160                            )}
     161
    88162
    89163                        </div>
    90164                    </div>
     165                    {(additionalFileUrls.length  === 0 && application.status === "UNDER_REVIEW" && application.response.length > 0 && auth.role == Roles.JOBSEEKER) &&
     166                        <div className="modal-buttons">
     167                            <div className="cancel-btn" onClick={toggleModal}> Cancel</div>
     168                            <button className="submit-btn"> Submit</button>
     169                        </div>}
    91170
    92171                </form>
     172
     173
    93174            </div>
    94175        </Modal>
  • jobvista-frontend/src/views/applications/Applications.css

    r0f0add0 r4d97b63  
    1717}
    1818
    19 /*.application-title span {*/
    20 /*    font-weight: bold;*/
    21 /*    */
    22 /*}*/
     19.response-message {
     20    background-color: floralwhite;
     21    border-radius: 8px;
     22    padding: 15px;
     23    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
     24}
     25
     26
     27
     28
     29.application-card-wrapper {
     30    margin: 15px 0;
     31    display: flex;
     32    flex-direction: column;
     33    transition: all 0.4s ease-in-out;
     34    gap: 3px;
     35}
     36
     37.application-card.changed {
     38    background-color: aliceblue;
     39}
     40
     41.application-card-wrapper .expand-section {
     42    max-height: 0;
     43    opacity: 0;
     44    transition: 0.5s ease;
     45    display: flex;
     46    flex-direction: column;
     47    align-items: flex-end;
     48    margin: 0 !important;
     49}
     50
     51.application-card-wrapper.expanded .expand-section {
     52    max-height: 200px;
     53    opacity: 1;
     54    transform: translateY(0);
     55    margin-top: 10px;
     56  /*  transition: max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;*/
     57}
     58
     59.expand-section textarea {
     60    width: 100%;
     61    padding: 10px;
     62    border-radius: 8px;
     63    border: 1px solid #ccc;
     64}
     65
    2366
    2467.application-card {
     
    2972    transition: all 0.3s ease;
    3073    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    31     height: auto;
     74    /*height: auto;*/
    3275    padding: 20px 20px;
    3376    display: flex;
    34     /*justify-content: center;*/
    35     /*gap: 20px;*/
    36     margin: 15px 0;
    37     /*z-index: -1000;*/
    3877}
    3978.application-card .app-job-seeker-pic {
     
    5190
    5291.application-card .app-info {
    53     width: 65%;
     92    width: 60%;
    5493    display: inline-flex;
    5594    flex-direction: column;
     
    86125
    87126.application-card .app-status {
    88     width: 28%;
     127    width: 38%;
    89128    display: inline-flex;
    90129    justify-content: end;
     
    98137}
    99138
    100 .status {
     139.app-status .status {
    101140    color: white;
    102141    padding: 5px 10px;
     
    105144    text-align: center;
    106145}
     146
     147
     148
     149
     150
     151
    107152
    108153
     
    168213    transform: rotate(-10deg);
    169214}
     215
     216.application-filters {
     217    gap: 15px;
     218    border-radius: 8px;
     219    background-color: white;
     220}
     221
     222/* Default Span Styles */
     223.application-filters span {
     224    padding: 8px 12px;
     225    border-radius: 5px;
     226    color: rgba(1,38,90,0.9);
     227    cursor: pointer;
     228    display: inline-flex;
     229    align-items: center;
     230}
     231
     232/* Icon Styles */
     233.application-filters span i {
     234    margin-right: 5px;
     235}
     236
     237/* Hover Effect */
     238.application-filters span:hover {
     239    background-color: rgba(1,38,90,0.9);
     240    color: white;
     241}
     242
     243.application-filters span:hover i {
     244    color: inherit; /* Makes the icon inherit the color from the parent span */
     245}
     246
     247/* Selected State */
     248.application-filters span.selected {
     249    background-color: rgba(1,38,90,0.9);
     250    color: white;
     251}
     252
     253.application-filters span.selected i {
     254    color: inherit; /* Ensures icon color matches the selected state */
     255}
  • jobvista-frontend/src/views/applications/ApplicationsByJobAd.js

    r0f0add0 r4d97b63  
    2525    const [jobAdTitle, setJobAdTitle] = useState("");
    2626
     27    const [changedApplications, setChangedApplications] = useState({});
     28
    2729    useEffect(() => {
    28         if(!dispatched && (applicationsByJobAdState.length === 0 || applicationsByJobAdState.length === 1)) {
     30        if(!dispatched) {
    2931            dispatch(ApplicationActions.fetchApplicationsByJobAdId(advertisement_id, (success, reponse) => {
    3032                if (success && reponse.data.length > 0) {
     
    6062    }, [dispatched])
    6163
     64
    6265    const fetchProfilePic = (jobSeekerId) => {
    6366        dispatch(JobSeekerActions.downloadProfilePic(jobSeekerId, (success, response) => {
     
    6770        }))
    6871    }
    69 
    7072
    7173    const options = [
     
    7678    ];
    7779
     80    const [selectedFilter, setSelectedFilter] = useState('All');
     81
     82    const filters = [
     83        { value: 'ALL', label: 'All', icon: 'fa-folder-open' },
     84        { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' },
     85        { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' },
     86        { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' },
     87        { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' },
     88    ];
     89
     90    const filterApplicationsByJobAdvertisement = (filter) => {
     91        dispatch(ApplicationActions.filterApplicationsByJobAdId(advertisement_id, filter, (success, response) => {
     92            if(success) {
     93                //notify
     94            }
     95        }))
     96    }
     97
    7898    let handleDefaultStatus = (status) => {
    7999        return options.find(option => option.value === status);
    80100    }
    81101
    82     let handleChangeStatus = (selectedOption, id) => {
    83         dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => {
     102    let handleStatusChange = (selectedOption, id) => {
     103
     104        const currentApplication = applicationsByJobAd.find(app => app.id === id);
     105
     106        setChangedApplications(prevState => ({
     107            ...prevState,
     108            [id]: {
     109                ...prevState[id],
     110                response: (selectedOption.value === "ACCEPTED" || selectedOption.value === "DENIED") ? (prevState[id]?.response || currentApplication.response || "") : "",
     111                status: selectedOption.value
     112            }
     113        }))
     114
     115       setApplicationsByJobAd(prevState => (
     116           prevState.map(application =>
     117           application.id === id ? {...application, status: selectedOption.value} : application)
     118       ))
     119
     120        const responseTextarea = document.getElementById(`response-${id}`);
     121        if (responseTextarea) {
     122            responseTextarea.value = "";
     123        }
     124
     125       /* dispatch(ApplicationActions.updateApplicationStatus(id, selectedOption.value, (success, response) => {
    84126            if(success) {
    85                 // console.log("Status updated.")
    86127                notifyAppStatusUpdate()
    87128            }
    88         }))
    89     }
     129        }))*/
     130    }
     131
     132    let handleResponseChange = (e, id) => {
     133
     134        const currentApplication = applicationsByJobAd.find(app => app.id === id);
     135
     136        setChangedApplications(prevState => ({
     137                ...prevState,
     138                [id]: {
     139                    ...prevState[id],
     140                    response: e.target.value,
     141                    status: prevState[id]?.status || currentApplication.status,
     142                }
     143            }
     144        ))
     145    }
     146
     147    const handleSaveChanges = () => {
     148        console.log(changedApplications)
     149       const changes = Object.entries(changedApplications).map(
     150           ([applicationId, change]) => ({
     151               id: applicationId,
     152               status: change.status,
     153               response: change.response,
     154           })
     155       );
     156        console.log(changes)
     157
     158       if(changes.length === 0) {
     159           return;
     160       }
     161
     162       dispatch(ApplicationActions.updateApplications(changes, (success, response) => {
     163           if(success) {
     164               setChangedApplications({});
     165               notifyAppStatusUpdate()
     166               //notify change success
     167           }
     168       }))
     169
     170
     171    }
     172
     173    const isChangedApplication = (id) => {
     174        return changedApplications && Object.keys(changedApplications).includes(id.toString());
     175    };
    90176
    91177
     
    98184        </div>
    99185
     186        <div className="row">
     187            <div className="col-md-6 application-filters-wrap">
     188                <div className="application-filters d-inline-flex flex-row justify-content-start">
     189                    {
     190                        filters.map(filter => (
     191                            <span
     192                                key={filter.label}
     193                                className={selectedFilter === filter.label ? "selected" : ""}
     194                                onClick={() => {
     195                                    setSelectedFilter(filter.label)
     196                                    filterApplicationsByJobAdvertisement(filter.value)
     197                                    setChangedApplications({});
     198                                }}
     199                            ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span>
     200                        ))
     201                    }
     202                </div>
     203
     204            </div>
     205            <div className="col-md-6 d-inline-flex flex-row justify-content-end">
     206                <button onClick={handleSaveChanges}
     207                        className={`blue-submit-button ${Object.keys(changedApplications).length === 0 ? 'disabled' : ''}`}
     208                        disabled={Object.keys(changedApplications).length === 0}
     209                >Submit Changes</button>
     210            </div>
     211        </div>
     212
     213
     214
    100215        {applicationsByJobAd && applicationsByJobAd.map((application, index) => (
    101             <div className="application-card">
    102                 <div className="app-job-seeker-pic">
    103                     <img
    104                         // loading gif
    105                         src={profilePicsState[application.jobSeekerId]}
    106                         alt=""
    107                         width={75} height={75}
    108                     />
    109                 </div>
    110                 <div className="app-info">
    111                     <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', {
    112                         day: 'numeric',
    113                         month: 'long',
    114                         year: 'numeric'
    115                     })}</b></span>
    116                     <div className="contact-info">
    117                         <div className="contact-item">
    118                             <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span>
    119                         </div> <div className="contact-item">
    120                             <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span>
    121                         </div> <div className="contact-item">
    122                             <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span>
     216            <div
     217                key={application.id}
     218                className={`application-card-wrapper ${(application.status !== "PROPOSED" ) ? 'expanded' : ''}`}
     219            >
     220
     221                <div className={`application-card ${changedApplications[application.id] ? 'changed' : ''}`}>
     222                    <div className="app-job-seeker-pic">
     223                        <img
     224                            src={profilePicsState[application.jobSeekerId]}
     225                            alt=""
     226                            width={75} height={75}
     227                        />
     228                    </div>
     229                    <div className="app-info">
     230                <span>Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', {
     231                    day: 'numeric',
     232                    month: 'long',
     233                    year: 'numeric'
     234                })}</b></span>
     235                        <div className="contact-info">
     236                            <div className="contact-item">
     237                                <i className="fa-solid fa-user"></i> <span>{application.jobSeekerName}</span>
     238                            </div>
     239                            <div className="contact-item">
     240                                <i className="fa-solid fa-envelope"></i> <span>{application.jobSeekerEmail}</span>
     241                            </div>
     242                            <div className="contact-item">
     243                                <i className="fa-solid fa-phone"></i> <span>{application.jobSeekerPhoneNumber}</span>
     244                            </div>
     245                        </div>
     246                    </div>
     247
     248                    <div className="app-status">
     249                        <ApplicationDetailsModal application={application} />
     250                        <div className="select">
     251                            <Select options={options} onChange={(selectedOption) => handleStatusChange(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)} />
    123252                        </div>
    124253                    </div>
    125254                </div>
    126255
    127                 <div className="app-status">
    128                     <ApplicationDetailsModal application={application}/>
    129                     <div className="select">
    130                         <Select options={options}  onChange={(selectedOption) => handleChangeStatus(selectedOption, application.id)} defaultValue={handleDefaultStatus(application.status)}/>
    131                     </div>
    132 
     256                <div className="expand-section">
     257                <textarea
     258                    id={`response-${application.id}`}
     259                    placeholder={application.status === "UNDER_REVIEW" ? "Request additional documents..." :"Write your response..."}
     260                    defaultValue={application.response}
     261                    onChange={(e) => handleResponseChange(e, application.id)}
     262                />
    133263                </div>
    134264            </div>
    135        ))}
     265        ))}
     266
    136267
    137268    </div>)
  • jobvista-frontend/src/views/applications/ApplicationsByJobSeeker.js

    r0f0add0 r4d97b63  
    2222
    2323
    24 
    2524    useEffect(() => {
    26         if(!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1) ) {
     25        if (!dispatched && (applicationsByJobSeekerState.length === 0 || applicationsByJobSeekerState.length === 1)) {
    2726            dispatch(ApplicationActions.fetchApplicationsByJobSeeker(auth.id, (success, response) => {
    28                 if(success && response.data.length > 0) {
     27                if (success && response.data.length > 0) {
    2928                    setApplicationsByJobSeeker(sortElementsBy(response.data, "submittedOn"));
    3029                }
     
    4140    useEffect(() => {
    4241
    43         if(dispatched && !logoDispatched) {
     42        if (dispatched && !logoDispatched) {
    4443            applicationsByJobSeeker.forEach(jobAd => {
    45                 if(jobAd.recruiterId && !logos[jobAd.recruiterId]) {
     44                if (jobAd.recruiterId && !logos[jobAd.recruiterId]) {
    4645                    fetchLogo(jobAd.recruiterId);
    4746                }
     
    4948            setLogoDispatched(true)
    5049            console.log("Fetch all logos GET")
    51         } else if (logoDispatched){
     50        } else if (logoDispatched) {
    5251            setLogos(logosState)
    5352            console.log("Fetch all logos STATE")
     
    5857
    5958
    60 
    6159    const fetchLogo = (recruiterId) => {
    6260        dispatch(RecruiterActions.downloadLogo(recruiterId, (success, response) => {
    63             if(success) {
     61            if (success) {
    6462                setLogos(prevLogos => ({...prevLogos, [recruiterId]: response}))
    6563            }
     
    6765    };
    6866
    69     const options = [
    70         {value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i className="fa-solid fa-paper-plane"></i> Proposed</span>},
    71         {value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i className="fa-solid fa-file-pen"></i> Under Review</span>},
    72         {value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i className="fa-solid fa-user-check"></i> Accepted</span>},
    73         {value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i className="fa-solid fa-user-slash"></i> Denied</span>}
     67    const options = [{
     68        value: 'PROPOSED', label: <span className="status" style={{backgroundColor: '#4A90E2'}}><i
     69            className="fa-solid fa-paper-plane"></i> Proposed</span>
     70    }, {
     71        value: 'UNDER_REVIEW', label: <span className="status" style={{backgroundColor: '#F5A623'}}><i
     72            className="fa-solid fa-file-pen"></i> Under Review</span>
     73    }, {
     74        value: 'ACCEPTED', label: <span className="status" style={{backgroundColor: '#7ED321'}}><i
     75            className="fa-solid fa-user-check"></i> Accepted</span>
     76    }, {
     77        value: 'DENIED', label: <span className="status" style={{backgroundColor: '#D0021B'}}><i
     78            className="fa-solid fa-user-slash"></i> Denied</span>
     79    }];
     80
     81    const [selectedFilter, setSelectedFilter] = useState('All');
     82
     83    const filters = [
     84        { value: 'ALL', label: 'All', icon: 'fa-folder-open' },
     85        { value: 'PROPOSED', label: 'Proposed', icon: 'fa-paper-plane' },
     86        { value: 'UNDER_REVIEW', label: 'Under Review', icon: 'fa-file-pen' },
     87        { value: 'ACCEPTED', label: 'Accepted', icon: 'fa-user-check' },
     88        { value: 'DENIED', label: 'Denied', icon: 'fa-user-slash' },
    7489    ];
     90
     91    const filterApplicationsByJobSeeker= (filter) => {
     92        dispatch(ApplicationActions.filterApplicationsByJobSeeker(auth.id, filter, (success, response) => {
     93            if(success) {
     94                //notify
     95            }
     96        }))
     97    }
    7598
    7699    let handleDefaultValue = (status) => {
     
    79102
    80103
    81 
    82     return (
    83         <div className="custom-container">
     104    return (<div className="custom-container">
    84105
    85106            <div className="application-title">
    86107                <h3>Application history</h3>
    87108            </div>
     109
     110        <div className="application-filters d-inline-flex flex-row justify-content-start">
     111            {
     112                filters.map(filter => (
     113                    <span
     114                        key={filter.label}
     115                        className={selectedFilter === filter.label ? "selected" : ""}
     116                        onClick={() => {
     117                            setSelectedFilter(filter.label)
     118                            filterApplicationsByJobSeeker(filter.value)
     119                        }}
     120                    ><i className={`fa-solid ${filter.icon}`}></i> {filter.label}</span>
     121                ))
     122            }
     123        </div>
     124
    88125            {applicationsByJobSeeker && applicationsByJobSeeker.map((application, index) => (
    89                 <div key={index} className="application-card">
    90                     <div className="app-company-logo">
    91                         <img
    92                             // loading gif
    93                             src={logosState[application.recruiterId]}
    94                             alt=""
    95                             width={75} height={75}
    96                         />
    97                     </div>
     126                <div className="application-card-wrapper">
     127                    <div key={index} className="application-card">
     128                        <div className="app-company-logo">
     129                            <img
     130                                // loading gif
     131                                src={logosState[application.recruiterId]}
     132                                alt=""
     133                                width={75} height={75}
     134                            />
     135                        </div>
    98136
    99                     <div className="app-info">
    100                         <Link to={`/job-advertisements/${application.jobAdId}`} className="jobAd-title">{application.jobAdTitle}</Link>
    101                         {/*<h5 className="jobAd-title"></h5>*/}
    102                         <div className="contact-info">
    103                             <div className="contact-item">
    104                                 <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span>
     137                        <div className="app-info">
     138                            <Link to={`/job-advertisements/${application.jobAdId}`}
     139                                  className="jobAd-title">{application.jobAdTitle}</Link>
     140                            {/*<h5 className="jobAd-title"></h5>*/}
     141                            <div className="contact-info">
     142                                <div className="contact-item">
     143                                    <i className="fa-solid fa-building"></i> <span>{application.recruiterName}</span>
     144                                </div>
     145                                <div className="contact-item">
     146                                    <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span>
     147                                </div>
     148                                <div className="contact-item">
     149                                    <i className="fa-solid fa-phone"></i>
     150                                    <span>{application.recruiterPhoneNumber}</span>
     151                                </div>
     152                                <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', {
     153                                    day: 'numeric', month: 'long', year: 'numeric'
     154                                })}</b></span>
    105155                            </div>
    106                             <div className="contact-item">
    107                                 <i className="fa-solid fa-envelope"></i> <span>{application.recruiterEmail}</span>
    108                             </div>
    109                             <div className="contact-item">
    110                                 <i className="fa-solid fa-phone"></i> <span>{application.recruiterPhoneNumber}</span>
    111                             </div>
    112                             <span> • Submitted on <b>{new Date(application.submittedOn).toLocaleString('default', {
    113                                 day: 'numeric',
    114                                 month: 'long',
    115                                 year: 'numeric'
    116                             })}</b></span>
     156                        </div>
     157
     158                        <div className="app-status">
     159                            <ApplicationDetailsModal application={application}/>
     160                            <> {handleDefaultValue(application.status).label}</>
     161                            {/*<div className="select">*/}
     162                            {/*    <Select isDisabled={true} options={options} />*/}
     163                            {/*</div>*/}
     164
    117165                        </div>
    118166                    </div>
     167                    {application.response &&
     168                        <div className="response-message">
     169                            {application.response}
     170                        </div>
     171                    }
    119172
    120                     <div className="app-status">
    121                         <ApplicationDetailsModal application={application}/>
    122                         <> {handleDefaultValue(application.status).label}</>
    123                         {/*<div className="select">*/}
    124                         {/*    <Select isDisabled={true} options={options} />*/}
    125                         {/*</div>*/}
     173                </div>
    126174
    127                     </div>
    128                 </div>
    129175            ))}
    130176
    131         </div>
    132     )
     177        </div>)
    133178}
  • jobvista-frontend/src/views/applications/ApplyToJobAdModal.js

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

    r0f0add0 r4d97b63  
    1 import {Button, TextField} from "@mui/material";
    21import {Link} from "react-router-dom";
    32import "./auth.css"
     
    98import {AuthActions} from "../../redux/actions/authActions";
    109import {notifyIncorrectEmailOrPassword} from "../../utils/toastUtils";
     10
     11import {GoogleOAuthProvider, GoogleLogin} from "@react-oauth/google";
    1112
    1213export const SignInForm = () => {
     
    4041    }
    4142
     43    const handleGoogleSuccess = (response) => {
     44        const tokenId = response.credential;
     45
     46        dispatch(AuthActions.signInGoogle(tokenId, (success, error) => {
     47            if (success) {
     48                console.log("User signed in successfully");
     49                if(success) {
     50                    navigate("/")
     51                }
     52            } else {
     53                console.error("Google sign-in failed", error);
     54            }
     55        }));
     56    };
     57
     58    const handleGoogleFailure = (error) => {
     59        console.error(error);
     60    };
     61
    4262    return (
    4363
    44         <div className="d-flex align-items-center">
     64        <div className="">
    4565            <div className="container">
    46                 <div className="row">
    47                     <div className="col-md-8 mx-auto form-container">
     66                <div className="row d-flex flex-column justify-content-center align-items-center">
     67                    <div className="col-md-8 form-container">
    4868                        <h3 className="login-heading mb-4">Sign in</h3>
    4969                        <form onSubmit={handleSubmit(signIn)}>
     
    83103
    84104                        <div className="row">
     105                            <GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}>
     106                                <GoogleLogin
     107                                    onSuccess={handleGoogleSuccess}
     108                                    onError={handleGoogleFailure}
     109                                    type={"standard"}
     110                                    text={"signin_with"}
     111                                    locale={"en"}
     112                                    redirectUri="http://localhost:3000/login/oauth2/code/google"
     113                                />
     114                            </GoogleOAuthProvider>
     115                        </div>
     116                        <br/>
     117                    </div>
     118
     119                    <div className="col-md-8 mt-5 form-container">
     120                        <div>
     121                            <h5 className="mb-3">Don't have an account?</h5>
     122                        </div>
     123
     124                        <div className="row">
    85125                            <div className="col-md-6">
    86126                                <Link to="/signup/recruiter" className="btn auth-secondary-btn text-uppercase fw-bold mb-2 w-100">SIGN UP AS RECRUITER</Link>
     
    91131                        </div>
    92132                    </div>
     133
    93134                </div>
    94135            </div>
  • jobvista-frontend/src/views/auth/auth.css

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

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

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

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

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